同步操作将从 古春波/java-construct 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
前言:文中出现的示例代码地址为:gitee代码地址
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 进程拥有共享的资源,如内存空间等,供其内部的线程共享 进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication) 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
在单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感 觉是同时运行的 。一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)
多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。
引用 Rob Pike 的一段描述:并发(concurrent)是同一时间应对(dealing with)多件事情的能力,并行(parallel)是同一时间动手做(doing)多件事情的能力
以调用方的角度讲,如果需要等待结果返回才能继续运行的话就是同步,如果不需要等待就是异步
多线程可以使方法的执行变成异步的,比如说读取磁盘文件时,假设读取操作花费了5秒,如果没有线程的调度机制,这么cpu只能等5秒,啥都不能做。
// 构造方法的参数是给线程指定名字,,推荐给线程起个名字
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
把【线程】和【任务】(要执行的代码)分开,Thread 代表线程,Runnable 可运行的任务(线程要执行的代码)Test2.java
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐给线程起个名字
Thread t2 = new Thread(task2, "t2");
t2.start();
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了,用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活。通过查看源码可以发现,方法二其实到底还是通过方法一执行的!
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况 Test3.java
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 实现多线程的第三种方法可以返回数据
FutureTask futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("多线程任务");
Thread.sleep(100);
return 100;
}
});
// 主线程阻塞,同步等待 task 执行完毕的结果
new Thread(futureTask,"我的名字").start();
log.debug("主线程");
log.debug("{}",futureTask.get());
}
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
Future提供了三种功能:
判断任务是否完成;
能够中断任务;
能够获取任务执行结果。
拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,是属于线程的私有的。当java中使用多线程时,每个线程都会维护它自己的栈帧!每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run(){
log.debug("我是一个新建的线程正在运行中");
FileReader.read(fileName);
}
};
thread.setName("新建线程");
thread.start();
log.debug("主线程");
}
输出:程序在 t1 线程运行, run()
方法里面内容的调用是异步的 Test4.java
11:59:40.711 [main] DEBUG com.concurrent.test.Test4 - 主线程
11:59:40.711 [新建线程] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
11:59:40.732 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] start ...
11:59:40.735 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 3 ms
将上面代码的thread.start();
改为 thread.run();
输出结果如下:程序仍在 main 线程运行, run()
方法里面内容的调用还是同步的
12:03:46.711 [main] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
12:03:46.727 [main] DEBUG com.concurrent.test.FileReader - read [test] start ...
12:03:46.729 [main] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 2 ms
12:03:46.730 [main] DEBUG com.concurrent.test.Test4 - 主线程
直接调用 run()
是在主线程中执行了 run()
,没有启动新的线程
使用 start()
是启动新的线程,通过新的线程间接执行 run()
方法 中的代码
InterruptedException
异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】sleep()
代替 Thread 的 sleep()
来获得更好的可读性yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
在主线程中调用t1.join,则主线程会等待t1线程执行完之后再继续执行 Test10.java
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
先了解一些interrupt()方法的相关知识:博客地址
sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态,以 sleep 为例Test7.java
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
log.debug("线程任务执行");
try {
Thread.sleep(10000); // wait, join
} catch (InterruptedException e) {
//e.printStackTrace();
log.debug("被打断");
}
}
};
t1.start();
Thread.sleep(500);
log.debug("111是否被打断?{}",t1.isInterrupted());
t1.interrupt();
log.debug("222是否被打断?{}",t1.isInterrupted());
Thread.sleep(500);
log.debug("222是否被打断?{}",t1.isInterrupted());
log.debug("主线程");
}
输出结果:(我下面将中断和打断两个词混用)可以看到,打断 sleep 的线程, 会清空中断状态,刚被中断完之后t1.isInterrupted()
的值为true
,后来变为false
,即中断状态会被清除。那么线程是否被中断过可以通过异常来判断。【同时要注意如果打断被join()
,wait()
blocked的线程也是一样会被清除,被清除(interrupt status will be cleared)的意思即中断状态设置为false
,被设置( interrupt status will be set)的意思就是中断状态设置为true
】
17:06:11.890 [Thread-0] DEBUG com.concurrent.test.Test7 - 线程任务执行
17:06:12.387 [main] DEBUG com.concurrent.test.Test7 - 111是否被打断?false
17:06:12.390 [Thread-0] DEBUG com.concurrent.test.Test7 - 被打断
17:06:12.390 [main] DEBUG com.concurrent.test.Test7 - 222是否被打断?true
17:06:12.890 [main] DEBUG com.concurrent.test.Test7 - 222是否被打断?false
17:06:12.890 [main] DEBUG com.concurrent.test.Test7 - 主线程
打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();
的返回值为true,可以判断Thread.currentThread().isInterrupted();
的值来手动停止线程
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。
如下所示:那么线程的isInterrupted()
方法可以取得线程的打断标记,如果线程在睡眠sleep
期间被打断,打断标记是不会变的,为false,但是sleep
期间被打断会抛出异常,我们据此手动设置打断标记为true
;如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true
。处理好这两种情况那我们就可以放心地来料理后事啦!
代码实现如下:
@Slf4j
public class Test11 {
public static void main(String[] args) throws InterruptedException {
TwoParseTermination twoParseTermination = new TwoParseTermination();
twoParseTermination.start();
Thread.sleep(3000); // 让监控线程执行一会儿
twoParseTermination.stop(); // 停止监控线程
}
}
@Slf4j
class TwoParseTermination{
Thread thread ;
public void start(){
thread = new Thread(()->{
while(true){
if (Thread.currentThread().isInterrupted()){
log.debug("线程结束。。正在料理后事中");
break;
}
try {
Thread.sleep(500);
log.debug("正在执行监控的功能");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
});
thread.start();
}
public void stop(){
thread.interrupt();
}
}
关于join的原理和这几个方法的对比:看这里
补充:
- sleep,join,yield,interrupted是Thread类中的方法
- wait/notify是object中的方法
sleep 不释放锁、释放cpu join 释放锁、抢占cpu yiled 不释放锁、释放cpu wait 释放锁、释放cpu
默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用t1.setDeamon(true);
方法变成守护线程
注意 垃圾回收器线程就是一种守护线程 Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
五种状态的划分主要是从操作系统的层面进行划分的
Thead thread = new Thead();
,还未与操作系统线程关联这是从 Java API 层面来描述的,我们主要研究的就是这种。状态转换详情图:地址 根据 Thread.State 枚举,分为六种状态 Test12.java
start()
方法之后的状态,注意,Java API 层面的 RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)BLOCKED
, WAITING
, TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节
详述线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了,下面举一个例子 Test13.java
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 1;i<5000;i++){
count++;
}
});
Thread t2 =new Thread(()->{
for (int i = 1;i<5000;i++){
count--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count的值是{}",count);
}
我将从字节码的层面进行分析:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
可以看到count++
和 count--
操作实际都是需要这个4个指令完成的,那么这里问题就来了!Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果代码是正常按顺序运行的,那么count的值不会计算错
出现负数的情况:
出现正数的情况:
一个程序运行多线程本身是没有问题的
问题出现在多个线程共享资源的时候
先定义一个叫做临界区的概念:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区
如
static int counter = 0;
static void increment()
{// 临界区
counter++;
}
static void decrement()
{// 临界区
counter--;
}
多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件
为了避免临界区中的竞态条件发生,由多种手段可以达到
现在讨论使用synchronized来进行解决,即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意 虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的: 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区的代码 同步是由于线程执行的先后,顺序不同但是需要一个线程等待其它线程运行到某个点。
synchronized(对象) // 线程1获得锁, 那么线程2的状态是(blocked)
{
临界区
}
上面的实例程序使用synchronized后如下,计算出的结果是正确!Test13.java
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
synchronized实际是 用对象锁保证了临界区中代码的原子性,临界区内的代对码对外是不可分割的,不会被线程切换所打断。
为了加深理解思考下面的问题:
如果吧synchronized(obj)放在for循环外面如何理解?
放在外面相当于给整个循环加上了锁那么就是保护了循环次数*++/--的指令这期间不会被其他线程干扰
如果t1 synchronized(obj1)但她synchronized(obj2)会怎么样?
不可以,t1,t2如果锁的不是一个对象,那么当发生上下文切换的时候,别的线程不会被阻塞
如果t1synchronized(obj)t2没有会怎么样?如何理解
t2不加锁的话他可以直接拿到共享变量,不会被阻塞住
synchronized实际上利用对象保证了临界区代码的原子性,临界区内的代码在外界看来是不可分割的,不会被线程切换所打断
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
//------------------------------------------------------------------------------------------------
class Test{
public synchronized static void test() {
}
}
// 等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
局部变量【局部变量被初始化为基本数据类型】是安全的,示例如下
public static void test1() {
int i = 10;
i++;
}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
如果局部变量引用的对象逃离方法的范围,那么要考虑线程安全的,代码示例如下 Test15.java
public class Test15 {
public static void main(String[] args) {
UnsafeTest unsafeTest = new UnsafeTest();
for (int i =0;i<100;i++){
new Thread(()->{
unsafeTest.method1();
},"线程"+i).start();
}
}
}
class UnsafeTest{
ArrayList<String> arrayList = new ArrayList<>();
public void method1(){
for (int i = 0; i < 100; i++) {
method2();
method3();
}
}
private void method2() {
arrayList.add("1");
}
private void method3() {
arrayList.remove(0);
}
}
无论哪个线程中的 method2 和method3 引用的都是同一个对象中的 list 成员变量:一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:
可以将list修改成局部变量,那么就不会有上述问题了
class safeTest{
public void method1(){
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
method2(arrayList);
method3(arrayList);}
}
private void method2(ArrayList arrayList) {
arrayList.add("1");
}
private void method3(ArrayList arrayList) {
arrayList.remove(0);
}
}
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会导致线程安全问题?情况1:有其它线程调用 method2 和 method3;情况2:在情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即如下所示: 从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为它们的每个方法是原子的
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
但注意它们多个方法的组合不是原子的,见下面分析
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
String
和Integer
类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的,有同学或许有疑问,String
有 replace
,substring
等方法【可以】改变值啊,其实调用这些方法返回的已经是一个新创建的对象了!
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
public Immutable add(int v){
return new Immutable(this.value + v);
}
}
分析线程是否安全,先对类的成员变量,类变量,局部变量进行考虑,如果变量会在各个线程之间共享,那么就得考虑线程安全问题了,如果变量A引用的是线程安全类的实例,并且只调用该线程安全类的一个方法,那么该变量A是线程安全的的。下面对实例一进行分析:此类不是线程安全的,MyAspect
切面类只有一个实例,成员变量start
会被多个线程同时进行读写操作
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
此例是典型的三层模型调用,MyServlet
UserServiceImpl
UserDaoImpl
类都只有一个实例,UserDaoImpl
类中没有成员变量,update
方法里的变量引用的对象不是线程共享的,所以是线程安全的;UserServiceImpl
类中只有一个线程安全的UserDaoImpl
类的实例,那么UserServiceImpl
类也是线程安全的,同理 MyServlet
也是线程安全的
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
跟示例二大体相似,UserDaoImpl
类中有成员变量,那么多个线程可以对成员变量conn
同时进行操作,故是不安全的
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
跟示例三大体相似,UserServiceImpl
类的update方法中 UserDao是作为局部变量存在的,所以每个线程访问的时候都会新建有一个UserDao
对象,新建的对象是线程独有的,所以是线程安全的
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
public abstract class Test {
public void bar() {
// 是否安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法,因为foo方法可以被重写,导致线程不安全。在String类中就考虑到了这一点,String类是finally
的,子类不能重写它的方法。
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
Test16.java
以 32 位虚拟机为例,普通对象的对象头结构如下,其中的Klass Word为指针,指向对应的Class对象;
数组对象
其中 Mark Word 结构为
所以一个对象的结构如下:
Monitor被翻译为监视器或者说管程
每个java对象都可以关联一个Monitor,如果使用synchronized
给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针
注意:synchronized 必须是进入同一个对象的 monitor 才有上述的效果不加 synchronized 的对象不会关联监视器,不遵从以上规则
代码如下 Test17.java
static final Object lock=new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
反编译后的部分字节码
0 getstatic #2 <com/concurrent/test/Test17.lock>
# 取得lock的引用(synchronized开始了)
3 dup
# 复制操作数栈栈顶的值放入栈顶,即复制了一份lock的引用
4 astore_1
# 操作数栈栈顶的值弹出,即将lock的引用存到局部变量表中
5 monitorenter
# 将lock对象的Mark Word置为指向Monitor指针
6 getstatic #3 <com/concurrent/test/Test17.counter>
9 iconst_1
10 iadd
11 putstatic #3 <com/concurrent/test/Test17.counter>
14 aload_1
# 从局部变量表中取得lock的引用,放入操作数栈栈顶
15 monitorexit
# 将lock对象的Mark Word重置,唤醒EntryList
16 goto 24 (+8)
# 下面是异常处理指令,可以看到,如果出现异常,也能自动地释放锁
19 astore_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return
注意:方法级别的 synchronized 不会在字节码指令中有所体现
轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是synchronized
,假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能
在轻量级的锁中,我们可以发现,如果同一个线程对同一个2对象进行重入锁时,也需要执行CAS操作,这是有点耗时滴,那么java6开始引入了偏向锁的东东,只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS了
一个对象的创建过程
如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的Thread,epoch,age都是0,在加锁的时候进行设置这些的值
偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0
来禁用延迟
注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
实验Test18.java,加上虚拟机参数-XX:BiasedLockingStartupDelay=0进行测试
public static void main(String[] args) throws InterruptedException {
Test1 t = new Test1();
test.parseObjectHeader(getObjectHeader(t));
synchronized (t){
test.parseObjectHeader(getObjectHeader(t));
}
test.parseObjectHeader(getObjectHeader(t));
}
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
测试禁用:如果没有开启偏向锁,那么对象创建后最后三位的值为001,这时候它的hashcode,age都为0,hashcode是第一次用到hashcode
时才赋值的。在上面测试代码运行时在添加 VM 参数-XX:-UseBiasedLocking
禁用偏向锁(禁用偏向锁则优先使用轻量级锁),退出synchronized
状态变回001
测试代码Test18.java 虚拟机参数-XX:-UseBiasedLocking
输出结果如下,最开始状态为001,然后加轻量级锁变成00,最后恢复成001
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
测试 hashCode
:当调用对象的hashcode方法的时候就会撤销这个对象的偏向锁,因为使用偏向锁时没有位置存hashcode
的值了
测试代码如下,使用虚拟机参数-XX:BiasedLockingStartupDelay=0
,确保我们的程序最开始使用了偏向锁!但是结果显示程序还是使用了轻量级锁。 Test20.java
public static void main(String[] args) throws InterruptedException {
Test1 t = new Test1();
t.hashCode();
test.parseObjectHeader(getObjectHeader(t));
synchronized (t){
test.parseObjectHeader(getObjectHeader(t));
}
test.parseObjectHeader(getObjectHeader(t));
}
输出结果
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
这里我们演示的是偏向锁撤销变成轻量级锁的过程,那么就得满足轻量级锁的使用条件,就是没有线程对同一个对象进行锁竞争,我们使用wait
和 notify
来辅助实现
代码 Test19.java,虚拟机参数-XX:BiasedLockingStartupDelay=0
确保我们的程序最开始使用了偏向锁!
输出结果,最开始使用的是偏向锁,但是第二个线程尝试获取对象锁时,发现本来对象偏向的是线程一,那么偏向锁就会失效,加的就是轻量级锁
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
会使对象的锁变成重量级锁,因为wait/notify方法之后重量级锁才支持
如果对象被多个线程访问,但是没有竞争,这时候偏向了线程一的对象又有机会重新偏向线程二,即可以不用升级为轻量级锁,可这和我们之前做的实验矛盾了呀,其实要实现重新偏向是要有条件的:就是超过20对象对同一个线程如线程一撤销偏向时,那么第20个及以后的对象才可以将撤销对线程一的偏向这个动作变为将第20个及以后的对象偏向线程二。Test21.java
建议先看看wait和notify方法的javadoc文档
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果,要点:
代码:Test22.java Test23.java这是带超时时间的
Test23.java中jiang'dao'de关于超时的增强,在join(long millis) 的源码中得到了体现:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
// join一个指定的时间
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
多任务版 GuardedObject图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。和生产者消费者模式的区别就是:这个生产者和消费者之间是一一对应的关系,但是生产者消费者模式并不是。rpc框架的调用中就使用到了这种模式。 Test24.java
要点
“异步”的意思就是生产者产生消息之后消息没有被立刻消费,而“同步模式”中,消息在产生之后被立刻消费了。
我们写一个线程间通信的消息队列,要注意区别,像rabbit mq等消息框架是进程间通信的。
它们是 LockSupport 类中的方法 Test26.java
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark;
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter, _cond和 _mutex
可以不看例子,直接看实现过程
1.先调用park
2.调用upark
RUNNABLE <--> WAITING
RUNNABLE <--> WAITING
RUNNABLE <--> WAITING
RUNNABLE <--> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
RUNNABLE <--> TIMED_WAITING
RUNNABLE <--> TIMED_WAITING
RUNNABLE <--> TIMED_WAITING
活跃性相关的一系列问题都可以用ReentrantLock进行解决。
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁t1 线程获得A对象锁,接下来想获取B对象的锁;t2 线程获得B对象锁,接下来想获取A对象的锁例。Test28.java
检测死锁可以使用 jconsole工具;或者使用 jps 定位进程 id,再用 jstack 定位死锁:Test28.java
下面使用jstack工具进行演示
D:\我的项目\JavaLearing\java并发编程\jdk8>jps
1156 RemoteMavenServer36
20452 Test25
9156 Launcher
23544 Jps
23848
22748 Test28
D:\我的项目\JavaLearing\java并发编程\jdk8>jstack 22748
2020-07-12 18:54:44
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.211-b12 mixed mode):
"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000000002a03800 nid=0x5944 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
//................省略了大部分内容.............//
Found one Java-level deadlock:
=============================
"线程二":
waiting to lock monitor 0x0000000002afc0e8 (object 0x00000000db9f76d0, a java.lang.Object),
which is held by "线程1"
"线程1":
waiting to lock monitor 0x0000000002afe1e8 (object 0x00000000db9f76e0, a java.lang.Object),
which is held by "线程二"
Java stack information for the threads listed above:
===================================================
"线程二":
at com.concurrent.test.Test28.lambda$main$1(Test28.java:39)
- waiting to lock <0x00000000db9f76d0> (a java.lang.Object)
- locked <0x00000000db9f76e0> (a java.lang.Object)
at com.concurrent.test.Test28$$Lambda$2/326549596.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"线程1":
at com.concurrent.test.Test28.lambda$main$0(Test28.java:23)
- waiting to lock <0x00000000db9f76e0> (a java.lang.Object)
- locked <0x00000000db9f76d0> (a java.lang.Object)
at com.concurrent.test.Test28$$Lambda$1/1343441044.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
有五位哲学家,围坐在圆桌旁。 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。 如果筷子被身边的人拿着,自己就得等待 Test29.java
当每个哲学家即线程持有一根筷子时,他们都在等待另一个线程释放锁,因此造成了死锁。这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情 况
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题下面我讲一下一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题,就是两个线程对两个不同的对象加锁的时候都使用相同的顺序进行加锁。 但是会产生饥饿问题Test29
顺序加锁的解决方案
相对于 synchronized 它具备如下特点
与 synchronized 一样,都支持可重入
基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
直接看例子:Test31.java
直接看例子:Test32.java
使用锁超时解决哲学家就餐死锁问题:Test33.java
synchronized锁中,在entrylist等待的锁在竞争时不是按照先到先得来获取锁的,所以说synchronized锁时不公平的;ReentranLock锁默认是不公平的,但是可以通过设置实现公平锁。本意是为了解决之前提到的饥饿问题,但是公平锁一般没有必要,会降低并发度,使用trylock也可以实现。
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待 ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
使用要点: Test34.java
本章我们需要重点掌握的是
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。