Java并发编程(多线程) -- 第一部分
一、Java线程
1. 四种创建和使用线程的方法
// 1. 最简单的 -- 直接使用Thread public static void test1() { Thread t1 = new Thread("t1") { @Override public void run() { log.info("t1 -- running..."); } }; t1.start(); } // 2. 使用Runnable接口 完成具体任务 public static void test2() { Runnable r = new Runnable() { @Override public void run() { log.info("t2 -- running..."); } }; Thread t2 = new Thread(r, "t2"); t2.start(); } // 3. 使用lambda表达式简化 @Test public static void test3() { Thread t3 = new Thread(() -> { log.info("t3 -- running..."); } , "t3"); t3.start(); }// 4. 使用FutureTask和Callable实现有返回值的线程使用 private static void test4() throws ExecutionException, InterruptedException { FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() { @Override public Integer call() throws Exception { log.info("FutureTask -- Callable -- runnable"); return 100; } }); Thread t4 = new Thread(task, "t4"); t4.start(); log.info("结果为:{}" , task.get()); // 10:14:45 [t4] c.Test1 - FutureTask -- Callable -- runnable // 10:14:45 [main] c.Test1 - 结果为:100 }
2. 通过Runnable接口使用线程的源码逻辑
逻辑:将Runnable接口中的run方法实现,并且将此Runnable接口作为Thread构造方法参数传入;
经过init()方法,将这个Runnable对象赋值给成员变量target;
然后JVM调用Thread的run()方法,从而调用Runnable接口中实现的run()方法,完成逻辑
// 创建线程,并且把传入的Runnbale接口作为init方法的参数public Thread(Runnable target, String name) { init(null, target, name, 0); }
// 再次传给init真正的实现方法 private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); }
// init内部具体实现private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { /* Determine if it's an applet or not */ /* If there is a security manager, ask the security manager what to do. */ if (security != null) { g = security.getThreadGroup(); } /* If the security doesn't have a strong opinion of the matter use the parent thread group. */ if (g == null) { g = parent.getThreadGroup(); } } /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */ g.checkAccess(); /* * Do we have the required permissions? */ if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();// =============== 关键代码 ================= //// 将传入的Runnbale接口赋值给成员变量,方便run接口使用 this.target = target; setPriority(priority); if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); }
// run方法拿到传入的Runnable接口,如果不为空,则直接调用其run方法@Override public void run() { if (target != null) { target.run(); } }
3. 通过FutureTask使用线程的源码逻辑
流程:创建FutureTask对象,构造方法中传入Callable接口,并且实现其call()方法;
构造方法会将这个已经实现了的Callable方法赋值给成员变量callable;
然后将此FutureTask对象作为Thread的参数,经过如Runnable接口相同的过程,JVM调用run方法();
最终调用FutureTask内部实现的run()方法,完成call()方法内部逻辑的执行和返回值的返回
// Callable接口里面只有一个call方法,且拥有返回值@FunctionalInterfacepublic interface Callable<V> { / * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception;}
// 因为间接继承了Runnable,所以可以被Thread使用public class FutureTask<V> implements RunnableFuture<V>...public interface RunnableFuture<V> extends Runnable, Future<V>... public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); // 将Callable赋值给成员变量,方便run()方法使用 this.callable = callable; this.state = NEW;// ensure visibility of callable }
public void run() { if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; try { // 得到刚才构造方法里面传入的Callable接口 Callable<V> c = callable; if (c != null && state == NEW) { V result; boolean ran; try { // 执行它的call方法,并且得到其返回值 result = c.call(); ran = true; } catch (Throwable ex) { result = null; ran = false; setException(ex); } if (ran) // 调用set()方法,将返回值设置给成员变量 set(result); } } finally { // runner must be non-null until state is settled to // prevent concurrent calls to run() runner = null; // state must be re-read after nulling runner to prevent // leaked interrupts int s = state; if (s >= INTERRUPTING) handlePossibleCancellationInterrupt(s); } }
// 将返回值传给outcome成员变量protected void set(V v) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = v; UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state finishCompletion(); } }
// 调用report()方法,进行最终的返回public V get() throws InterruptedException, ExecutionException { int s = state; if (s <= COMPLETING) s = awaitDone(false, 0L); return report(s); }
// 将outcome赋值给x,然后最终返回private V report(int s) throws ExecutionException { Object x = outcome; if (s == NORMAL) return (V)x; if (s >= CANCELLED) throw new CancellationException(); throw new ExecutionException((Throwable)x); }
二、常用方法
1. run()方法和start()方法
- 如果直接调用run()方法,相当于直接调用了一个普通方法,所以不会创建一个新的线程,而是由调用这个方法的线程去执行。
- 如果调用start()方法,就会通过本地方法创建一个线程,然后使用这个线程去执行它的run()方法。调用start()方法之后的该线程的状态转换如下
Thread t1 = new Thread(() -> { log.info("方法被执行了。。。"); }, "t1"); System.out.println(t1.getState()); // NEW t1.start(); System.out.println(t1.getState()); // RUNNABLE
2. sleep()方法和yield()方法
2.1 sleep
让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间内,该线程不会获得执行的机会,而其它任何优先级的线程都可以得到执行的机会,即使系统中没有其它可执行的线程,处于sleep()的线程也不会执行,sleep()是用来暂停线程的执行。
注意:sleep方法不会释放锁,可以理解为抱着锁(资源)睡觉,但此时会让出cup以调度其他任务
- 调用sleep()方法会让当前线程从Running状态进入Timed Waiting状态()阻塞
- 如果其他线程调用internet()方法打断其他正在睡眠的线程,这时sleep()方法会抛出InterruptedException
- 睡眠结束后,此线程恢复到Runnable状态,争抢cpu资源,所以未必会立刻得到执行
- 可以使用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
2.2 yield
yield()方法是一个和sleep()方法有点相似的方法,它也是Thread类提供的一个静态方法。可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将该线程转入就绪状态。yeild()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()线程暂停之后,线程调度器又将其调度出来重新执行。
当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行机会。
- 调用yield会让当前线程从Running状态进入Runnable就绪状态,然后调度执行其他线程
- 具体的实现依赖于操作系统的任务调度器。可能在调用yield()方法之后,其他线程还是没有被分配时间片,所以当前线程依旧可以继续执行
2.3 线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
- 也就是说,线程优先级在某种情况下可能会影响线程的执行顺序(频率),但是这不是一定的;有时优先级较低的线程也可能会比优先级高的线程执行过多次
2.4 应用
我们在程序执行过程中,可能回需要使用while(true)这种写法,如服务器通信过程;此时如果没有优化,cpu的占用率会非常的高(尤其是核心数较小的情况);所以为了防止cpu的浪费,我们可以使用 yield 或 sleep 来让出 cpu 的使用权给其他程序。
while(true) {try {// ===== 逻辑代码 =====......// ==================Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}
wait,sleep,yield方法区别
3. join()方法
1. 基本使用
static int num = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } num = 10; }, "t1");t1.start(); log.info("num: {}", num); // 16:26:29 [main] c.Test2 - num: 0 }
分析:此时num的值输出为0而不是10,因为主线程和t1线程是异步执行的,当t1线程给num赋值为10时,主线程已经执行完打印操作了,所以最后结果为0
我们可以使用join()方法实现同步操作,即当t1执行完之后,在进行打印操作
static int num = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } num = 10; }, "t1"); t1.start(); t1.join(); log.info("num: {}", num); // 16:31:52 [main] c.Test2 - num: 10 }
2. 多线程调用使用
static int num1 = 0; static int num2 = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } num1 = 10; }, "t1"); Thread t2 = new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } num2 = 20; }, "t2"); final long start = System.currentTimeMillis(); t1.start(); t2.start(); t1.join(); t2.join(); final long end = System.currentTimeMillis(); // 16:43:27 [main] c.Test2 - num1:10, num2:20, time:2002 log.info("num1:{}, num2:{}, time:{}", num1, num2, end-start); }
分析:用时为2秒而不是3秒,因为t1和t2线程同时开始,当t1线程睡眠1秒时,t2线程也异步睡眠了1秒,当t1执行完毕之后,t2会继续睡眠1秒,共2秒
反过来t1睡眠2秒时,t2也会同时先睡眠1秒,这时t2会先执行完毕,然后等待t1线程睡眠2秒完毕后,t1和t2线程都已经执行完毕,无需等待,共2秒
3. 有时效的join
等待设置的时间数,如果超过此时间还线程没有结束,则结束等待,继续执行当前线程内容;如果在此时间前线程已经结束,则直接结束等待,继续执行当前线程内容
4.interrupt()方法
打断 sleep,wait,join 的线程;这几个方法都会让线程进入阻塞状态;打断 sleep 的线程, 会清空打断状态
1.打断睡眠中的线程:会导致线程被打断,抛出异常,并且将打断状态重置为false
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1"); t1.start(); Thread.sleep(1000); t1.interrupt(); // 19:35:49 [main] c.Test2 - t1的打断状态为:false // 报错 java.lang.InterruptedException: sleep interrupted log.info("t1的打断状态为:{}", t1.isInterrupted()); }
2. 打断正常运行中的线程:会抛出异常,将打断状态置为true,从而在线程内部通过判断来优雅的终止线程(料理后事)
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while (true) { final boolean interrupted = Thread.currentThread().isInterrupted(); if (interrupted) { // 19:39:36 [t1] c.Test2 - 被打断了...... log.info("被打断了......"); break; } } }, "t1"); t1.start(); Thread.sleep(500); t1.interrupt(); // 19:39:36 [main] c.Test2 - t1的打断状态为:true log.info("t1的打断状态为:{}", t1.isInterrupted()); }
3. 两阶段终止模式
1. interrupt() 直接打断
@Slf4j(topic = "c.Test5")public class Test5 { public static void main(String[] args) throws InterruptedException { TPTInterrupt interrupt = new TPTInterrupt(); interrupt.start(); Thread.sleep(4500); interrupt.stop(); }}@Slf4j(topic = "c.TPTInterrupt")class TPTInterrupt { Thread thread; public void start() { thread = new Thread(() -> { while (true) { final Thread currentThread = Thread.currentThread(); if (currentThread.isInterrupted()) { log.info("料理后事"); break; } try { Thread.sleep(1000); // 在这里打断,就是打断睡眠中的线程 log.info("监控线程......"); // 在这里打断就是打断正常运行的线程 } catch (InterruptedException e) { e.printStackTrace(); // 打断睡眠中的线程,会抛出异常并且将打断标记重置为false // 这样会导致循环还是无法终止,所以需要在内部进行打断操作 // 将打断标记置为true,从而跳出循环,结束线程 currentThread.interrupt(); } } }); thread.start(); } public void stop() { thread.interrupt(); }}
2. 使用打断标记打断(volatile)
public class Test19 { public static void main(String[] args) throws InterruptedException { TPTVolatile tptVolatile = new TPTVolatile(); tptVolatile.start(); Thread.sleep(3500); System.out.println("停止监控"); tptVolatile.stop(); }}class TPTVolatile { private Thread thread; private volatile boolean stop = false; public void start() { thread = new Thread(() -> { while (true) { if (stop) { System.out.println(Thread.currentThread().getName() + " 料理后事"); break; } try { Thread.sleep(1000); System.out.println("执行监控记录"); } catch (InterruptedException e) { System.out.println("打断监控线程"); } } }, "监控线程"); thread.start(); } public void stop() { stop = true; thread.interrupt(); }}
4. 打断 park 线程
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { log.info("park....."); LockSupport.park(); log.info("unpark....."); // 调用Thread.interrupted()方法,会将打断状态置为false,这样接下来还可以将现场park // 如果不将打断状态置为false,则后续就不能将现场park了 log.info("打断状态为:{}", Thread.interrupted()); LockSupport.park(); log.info("unpark....."); }, "t1"); t1.start(); Thread.sleep(1000); t1.interrupt(); /*20:21:54 [t1] c.Test2 - park.....20:21:55 [t1] c.Test2 - unpark.....20:21:55 [t1] c.Test2 - 打断状态为:true*/ }
三、线程分类和线程状态
1. 线程分类(主线程和守护线程)
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
垃圾回收器线程就是一种守护线程;
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等
待它们处理完当前请求
2. 线程状态
1. 五种状态(操作系统层面)
2. 六种状态(java API层面)
java代码6中状态演示
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { log.info("running"); }, "t1"); Thread t2 = new Thread(() -> { while (true) { } }, "t2"); t2.start(); Thread t3 = new Thread(() -> { log.info("running"); }, "t3"); t3.start(); Thread t4 = new Thread(() -> { synchronized (Test7.class) { try { Thread.sleep(10000000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t4"); t4.start(); Thread t5 = new Thread(() -> { try { t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } }, "t5"); t5.start(); Thread t6 = new Thread(() -> { synchronized (Test7.class) { try { Thread.sleep(10000000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t6"); t6.start(); Thread.sleep(1000); System.out.println(t1.getState()); System.out.println(t2.getState()); System.out.println(t3.getState()); System.out.println(t4.getState()); System.out.println(t5.getState()); System.out.println(t6.getState()); /* NEW RUNNABLE TERMINATED TIMED_WAITING WAITING BLOCKED */ }
3. 状态转换过程(详细)
1. NEW --> RUNNABLE
当调用 t.start()
方法时,由 NEW --> RUNNABLE
2. RUNNABLE WAITING
t 线程调用了 synchronized(obj)
获取了锁对象后
- 调用
obj.wait()
方法时,t 线程 进入 waitSet 等待,从 RUNNABLE --> WAITING - 调用
obj.notify()
,obj.notifyAll()
,t.interrupt()
方法时- 如果锁竞争成功, t 线程 进入了 Owner, 从 WAITING --> RUNNABLE
- 如果锁竞争失败, t 线程 进入了 EntryList,从 WAITING --> BLOCKED
3. RUNNABLE WAITING
当前线程调用了t.join()
方法时,当前线程从 RUNNABLE --> WAITING
- 注意当前线程在t 线程对象的监视器上等待
t 线程运行结束,或者调用了当前线程的 interrupt()
方法时,当前线程从 WAITING --> RUNNABLE
4. RUNNABLE WAITING
当前线程调用 LockSupport.park()
方法会让当前线程从 RUNNABLE --> WAITING
调用 LockSupport.unpark(目标线程)
或调用了线程 的 interrupt()
,会让目标线程从 WAITING -->
RUNNABLE
5. RUNNABLE TIMED_WAITING
t 线程调用了 synchronized(obj)
获取了锁对象后
- 调用
obj.wait(long n)
方法时,t 线程 进入 waitSet 等待,从 RUNNABLE --> TIMED_WAITING - t 线程等待时间超过了 n 毫秒,或调用
obj.notify()
,obj.notifyAll()
,t.interrupt()
方法时- 如果锁竞争成功, t 线程 进入了 Owner, 从 TIMED_WAITING --> RUNNABLE
- 如果锁竞争失败, t 线程 进入了 EntryList,从 TIMED_WAITING --> BLOCKED
6. RUNNABLE TIMED_WAITING
当前线程调用了t.join(long n)
方法时,当前线程从 RUNNABLE --> TIMED_WAITING
- 注意当前线程在t 线程对象的监视器上等待
当前线程等待时间超过了n毫秒,或t 线程运行结束,或者调用了当前线程的 interrupt()
方法时,当前线程从 TIMED_WAITING --> RUNNABLE
7. RUNNABLE TIMED_WAITING
当前线程调用 Thread.sleep(long n)
,当前线程从 RUNNABLE --> TIMED_WAITING
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
8. RUNNABLE TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos)
或 LockSupport.parkUntil(long millis)
时,当前线程从 RUNNABLE --> TIMED_WAITING
调用 LockSupport.unpark(目标线程)
或调用了线程 的 interrupt()
,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
9. RUNNABLE BLOCKED
t 线程用 synchronized(obj)
获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
10. RUNNABLE TERMINATED
当前线程所有代码运行完毕,进入TERMINATED
管程(阻塞)-悲观锁
四、synchronized讲解
1. 共享资源带来的问题
private static int num = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { num++; } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { num--; } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); // 15:40:15 [main] c.Test2 - 结果num:1039 log.info("结果num:{}", num); }
分析原因:
在jvm层面i++
操作等价于如下四个字节码指令
getstatic i // 获取静态变量i的值iconst_1 // 准备常量1iadd // 自增putstatic i // 将修改后的值存入静态变量i
类似的i--
也有类似的四个字节码指令
getstatic i // 获取静态变量i的值iconst_1 // 准备常量1isub // 自减putstatic i // 将修改后的值存入静态变量i
在单线程环境中,字节码指令按顺序执行,不会存在乱序行为,所以不会产生错误的结果。 如下方正确执行流程
在多线程环境中,i++和i–的字节码指令可能会存在执行到一半,时间片刚好用完,再去执行另一个操作,这样就会导致字节码指令交错运行导致最终结果出错。 如下方交错执行字节码指令导致最终结果为错误的 -1
2. 临界区
如果多个线程访问共享资源时:如果只有读取共享资源的操作不会产生问题;但是如果多个线程对共享资源进行了读写操作时发生了指令交错,就会产生问题
一段代码块内如果存在对共享资源的多线程读写操作,这段代码块则被称为临界区
如上面代码中的 num++
和 num--
所在的代码块就是临界区
竞态条件:多个线程在执行临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
3. synchronized解决方案
synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
private static int num = 0; private static Object obj = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { synchronized (obj) { num++; } } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { synchronized (obj) { num--; } } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); // 16:25:39 [main] c.Test2 - 结果num:0 log.info("结果num:{}", num); }
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
4. 用面向对象的思维进行改进
public static void main(String[] args) throws InterruptedException { Room room = new Room(); Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { room.increment(); } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { room.decrement(); } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); // 16:41:34 [main] c.Test2 - 结果num:0 log.info("结果num:{}", room.getNum()); }}class Room { private int num = 0; public void increment() { synchronized (this) { num++; } } public void decrement() { synchronized (this) { num--; } } public int getNum() { synchronized (this) { return this.num; } }}
5. 方法上的synchronized区别
- 如果加在普通方法上,就代表锁的事当前对象
class Test{public synchronized void test() {}}// 等价于class Test{public void test() {synchronized(this) {}}}
- 如果加在静态方法上,就代表锁的是当前类对象(大的Class对象)
class Test{public synchronized static void test() {}}// 等价于class Test{public static void test() {synchronized(Test.class) {}}}
- 如果方法上没有加synchronized,就代表这个方法不受锁的限制,可以任意执行
五、synchronized引发的八锁问题(重点)
1. 八种情况
- 此时两个方法锁的是同一个实例对象,所以结果为12 或 21
public class Test9 { public static void main(String[] args) { Number n1 = new Number(); new Thread(()->{ n1.a(); }).start(); new Thread(()->{ n1.b(); }).start(); }}class Number{ public synchronized void a() { log.debug("1"); } public synchronized void b() { log.debug("2"); }}
- 此时锁的还是同一个实例对象,所以结果为1s后12,或 2 1s后 1
class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();}
- 此时a方法和b方法带有锁且锁的是同一个实例对象,而c方法没有任何锁限制。所以结果为3 1s 12 或 23 1s 1 或 32 1s 1
class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}public void c() {log.debug("3");}}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();new Thread(()->{ n1.c(); }).start();}
- 这时a方法和b方法锁的是同一个实例对象,而主方法中声明的是两个不同的实例对象,所以此时两线程中的a和b方法锁的不是同一个实例对象,结果为:2 1s 后 1
class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();}
- 此时a方法为静态方法,锁的是类对象,而b方法锁的是实例对象。所以此时的调用锁住的并不是同一个对象,结果为:2 1s 后 1
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();}
- 此时的a和b方法都为静态方法,锁的都是同一个类对象,所以结果为:1s 后12, 或 2 1s后 1
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public static synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();}
- a方法为静态方法,b方法为普通方法,他们锁的并不是同一个对象,所以结果为:2 1s 后 1
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();}
- 此时的a和b方法都为静态方法,锁的都是同一个类对象,即使调用者并不是同一个实例对象,但是类对象只有一个,所以结果为:1s 后12, 或 2 1s后 1
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public static synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();}
2. 总结
一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一 synchronized ,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一 一个线程可以去访间这 synchronized 方法。锁的是当对象this,被锁定后,其它的线程都不能进入到当前对象的其它的 synchronized 方法。(普通方法与 synchronized 方法无关)
所有的非静态同步方法用的都是同一把锁 即 实例对象本身
synchronized 实现同步的基础:Java中的每一个对象都可以作为
具体表现为以下3种形式
- 对于普通同步方法,是当前实例对象。new的对象
- 对于静态同步方法,镜是当前类的 class对象。class类
- 对于同步方法块,是 synchronized 括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以不需要等待该实例对象已获取锁的静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是问一把锁 即类对象本身
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是问一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们是同一个类的实例对象。
六、线程安全性分析
成员变量和静态变量是否线程安全?
- 如果它们没有被共享,则线程安全
- 如果他们被共享了:如果只有读操作,则线程安全;如果有读写操作,则这段代码为临界区,需要考虑线程安全问题
局部变量是否线程安全?
- 局部变量是线程安全的
- 但是局部变量引用的对象不一定:如果该对象没有逃离方法的作用范围,他就是线程安全的
- 如果该对象逃离了方法的作用范围,则需要考虑他的线程安全问题
1. 局部变量线程安全分析
安全情况
// 此时的i变量为普通变量,存放在局部变量表中,线程私有,线程安全public static void test1() {int i = 10;i++;}
成员变量的不安全情况
class ThreadUnsafe {ArrayList<String> list = new ArrayList<>();public void method1(int loopNumber) {for (int i = 0; i < loopNumber; i++) {// 临界区, 会产生竞态条件// 方法2和方法3执行的顺序不能确定,list为共享资源method2();method3();}}private void method2() {list.add("1");}private void method3() {list.remove(0);}}
改为局部变量安全的情况
class ThreadSafe {public final void method1(int loopNumber) {// 每个线程调用method1方法时都会创建一个list// 且这个list并不会逃离方法作用范围,外界无法访问// 所以这是的list就是线程安全的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 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();}}
2. 常见的线程安全类
StringIntegerStringBufferRandomVectorHashtablejava.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的
线程安全情况,多个线程对同一实例调用同一方法,会有synchronized保证线程安全
Hashtable table = new Hashtable();new Thread(()->{table.put("key", "value1");}).start();new Thread(()->{table.put("key", "value2");}).start();
线程不安全情况,多个线程对同一实例调用不同方法的组合,可能会导致线程不安全情况的发生
// 这里本身想要只添加一个元素// 但是由于不同方法的组合,会造成两次put方法的调用Hashtable table = new Hashtable();// 线程1,线程2if( table.get("key") == null) {table.put("key", value);}
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。他们保证线程安全的方法就是:
它们会在底层重新创建新的变量然后再复制给原来的变量,这样就不会对原来的变量进行直接修改,这样就保证了线程安全
public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }
3. 线程安全性分析示例
例一:
public class MyServlet extends HttpServlet {// 是否安全? // 不安全,Map实现类中Hashtable是线程安全的,HashMap不是Map<String,Object> map = new HashMap<>();// 是否安全?// 安全,String为不可变类,线程安全String S1 = "...";// 是否安全?// 安全,String为不可变类,线程安全final String S2 = "...";// 是否安全?// 不安全,Date不在线程安全类中,所以线程不安全Date D1 = new Date();// 是否安全?// 不安全,Date本身不是线程安全的,final只是使他的引用不可变,它其中的变量还是可变的final Date D2 = new Date();public void doGet(HttpServletRequest request, HttpServletResponse response) {// 使用上述变量}}
例二:
public class MyServlet extends HttpServlet {// 是否安全?// 不安全,因为这时UserService为成员变量,多线程共用一份,线程不安全// 并且update方法中的count也是成员变量,会有多个线程同时调用修改,线程不安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}}public class UserServiceImpl implements UserService {// 记录调用次数private int count = 0;public void update() {// ...count++;}}
例三:
@Aspect@Componentpublic class MyAspect {// 是否安全?// spring内的对象,不做额外说明都是单例的,所以start只有一份// 多线程共享start变量,并对它进行赋值修改,会造成线程不安全问题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));}}
例四:
public class MyServlet extends HttpServlet {// 是否安全// 安全,因为无论是Service还是Dao中,都没有可以修改的变量// 虽然是成员变量且多线程共用一份,但是没有可修改的地方,线程安全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) {// ...}}}
例五:
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 {// 是否安全// 不安全,因为是成员变量,所以多线程共用一份,所以不安全// 如线程1刚刚给conn赋值,还没来得及使用,线程2将conn close掉了,造成了线程不安全问题private Connection conn = null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("","","");// ...conn.close();}}
例六:
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 {// 是否安全// 安全,因为观察到UserServiceImpl 中update方法里面每次都会创建一个单独的userDaoImpl// 所以Connection对象在每个线程中单独创建一个新的,不会造成线程不安全问题private Connection conn = 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() {// 是否安全// 不安全,因为sdf作为另一个抽象方法的参数传递了出去// 逃离了方法的作用范围,可能再方法外交给另外的线程使用// 这样就使sdf变成了共享资源,线程不安全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 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
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();}}
七、Monitor概念
1. Java对象头
包含两部分
- 运行时元数据
- 哈希值( HashCode )
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针:指向类元数据的InstanceKlass,确定该对象所属的类型
- 说明:如果是数组,还需记录数组的长度
普通对象:
|--------------------------------------------------------------|| Object Header (64 bits) ||------------------------------------|-------------------------|| Mark Word (32 bits) | Klass Word (32 bits) ||------------------------------------|-------------------------|
数组对象:
|---------------------------------------------------------------------------------|| Object Header (96 bits) ||--------------------------------|-----------------------|------------------------|| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) ||--------------------------------|-----------------------|------------------------|
32位虚拟机的MarkWord结构:
|-------------------------------------------------------|--------------------|| Mark Word (32 bits) | State ||-------------------------------------------------------|--------------------|| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal ||-------------------------------------------------------|--------------------|| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased ||-------------------------------------------------------|--------------------|| ptr_to_lock_record:30 | 00 | Lightweight Locked ||-------------------------------------------------------|--------------------|| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked ||-------------------------------------------------------|--------------------|| | 11 | Marked for GC ||-------------------------------------------------------|--------------------|
64位虚拟机的MarkWord结构:
|--------------------------------------------------------------------|--------------------|| Mark Word (64 bits) | State ||--------------------------------------------------------------------|--------------------|| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal ||--------------------------------------------------------------------|--------------------|| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased ||--------------------------------------------------------------------|--------------------|| ptr_to_lock_record:62 | 00 | Lightweight Locked ||--------------------------------------------------------------------|--------------------|| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked ||--------------------------------------------------------------------|--------------------|| | 11 | Marked for GC ||--------------------------------------------------------------------|--------------------|
2. Monitor原理
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
- 刚开始 Monitor 中 Owner 为 null
- 当 线程1 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 线程1,且Monitor中只能有一个 Owner
- 在 线程1 上锁的过程中,如果 线程2,线程3 也来执行 synchronized(obj),就会进入EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,将obj锁对象的MarkWord重置,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时候是非公平的
- 图中 WaitSet 中记录的是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析
注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
3. synchronized原理(字节码级别的Monitor)
0: getstatic #2 // <- lock引用(obj) (synchronized开始)3: dup4: astore_1 // lock引用(复制一份存储起来,为解锁(恢复MarkWord)准备) -> slot 15: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针6: getstatic #3 // <- i9: iconst_1 // 准备常数 110: iadd // +111: putstatic #3 // -> i14: aload_1 // 加载lock引用,解锁并重置MarkWord <- lock引用15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList16: goto 2419: astore_2 // 出现异常的情况(19--23):e -> slot 220: aload_1 // <- lock引用21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList22: aload_2 // <- slot 2 (e)23: athrow // throw e24: return
4. synchronized原理进阶
锁升级过程
- 对象刚刚被创建,为无锁状态
- 当这个锁对象第一次被线程调用后,偏向锁会偏向这个第一个获得它的线程,接下来的执行过程中,如果该锁没有被其他线程获取的话,则持有的偏向锁不需要更改。
- 当有另外的线程加入来抢占锁时,会先撤销偏向锁,并且将偏向锁升级为轻量级锁。各个线程一同来抢占这把锁,如果一个线程抢占成功,则其他线程在原地自旋等待,直到抢占到锁资源。
- 当某个线程自旋次数超过10次,或者线程数到达cpu核数的1/2,则升级为重量级锁。此时是向JVM申请的锁,谁获得了这把锁,则进去执行逻辑;其他的线程不做自旋(这里也可以进行自旋优化,通过短时间的自旋,尝试获取一下锁,避免只要拿不到锁就直接被阻塞),而是直接进入到当前锁的等待队列(无顺序)中放着(不会消耗cpu资源),当锁被释放后,再从队列中调度其他线程获取锁并执行逻辑。
锁升级过程解释
1. 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}}public static void method2() {synchronized( obj ) {// 同步块 B}}
-
创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
-
当线程获取锁对象的时候,首先会让Lock Record中的锁对象引用地址指向锁对象,然后通过CAS替换锁对象的MarkWord和Lock Record中的锁记录
-
CAS有两种失败的情况
- 第一种就是当想要进行交换时,发现其他线程已经获取了锁对象
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
-
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
-
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功,恢复到无锁(或者偏向锁)状态
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2. 锁膨胀(重量级锁)
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,线程自旋超过了10次,或者线程数量到达cpu数量的二分之一,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
-
当 线程2 进行轻量级加锁时,线程1 已经对该对象加了轻量级锁,线程1在原地一直自旋等待
-
当竞争线程足够多 或者 自选次数超过限度之后, 进入锁膨胀流程,升级为重量级锁
- 即为 Object 对象申请 Monitor 锁,让 MarkWord 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED
-
当 线程1 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,将锁对象的MarkWord保存到Monitor中,并且唤醒 EntryList 中 BLOCKED 线程
3. 自旋优化
- 锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
注意:《java并发编程艺术》一书中提到重量级锁不使用自旋,有待商榷。
我认为这里重量级锁也可以进行自旋优化,
4. 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销;
由于hashCode在被使用前不会产生也就不用存储,而此时调用了对象的hashCode方法,产生了hashCode,偏向锁中没有地方可以存储这个hashCode, 所以这时会撤销偏向锁,变为普通的无锁态。
5. 锁消除、锁粗化
锁消除
类似下面的代码中的getNum()方法,在运行过程中,对象obj为局部变量,并且没有离开方法的作用范围,他不可能被其他线程引用,所以obj不可能成为共享资源;JVM会自动消除包裹在其中的synchronized锁,以增加运行效率
public class Test {public void getNum() {Object obj = new Object();synchronized(this) {obj = null;obj = new Object();}}}
锁粗化
例如下面的代码,for循环里执行了100此append方法,每次执行都会判断synchronized加锁情况,效率很低;这是JVM会将加锁的范围粗化到这一系列操作(如循环)的外部,使得这一系列操作只需加一次锁即可
public String getString(String str) {StringBuild sb = new StringBuild();for (int i = 0; i < 100; i++) {sb.append(str);}return sb.toString();}
八、wait/notify方法
1. 原理
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
2. API
obj.wait()
让进入 object 监视器的线程到 waitSet 等待obj.notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒
注意:这里的wait方法和notify、notifyAll方法,它们都是线程之间进行协作的手段,都属于 Object 对象的方法。都需要在获得锁的前提下才能使用,否则会抛出异常
【重点】sleep()和wait()方法的比较:
- sleep() 使 Thread 的方法;而 wait() 是 Object 的方法
- sleep() 不需要强制配合 synchronized 使用;而 wait() 需要和 synchronized 一起使用
- sleep() 在睡眠的同时,不会释放锁对象;而 wait() 在等待的时候会释放锁对象
- 他们两个方法调用后的状态都是 WAITING
3. 使用方法
synchronized(lock) {// 避免虚假唤醒,用while,不满足条件继续waitwhile(条件不成立) {lock.wait();}// 干活}//另一个线程synchronized(lock) {lock.notifyAll();}
4. 保护性暂停模式(超时等待模式)
1. 案例1(自定义)
public class Test9 { public static void main(String[] args) throws InterruptedException { GuardedObject guardedObject = new GuardedObject(); new Thread(() -> { // 等待结果 log.info("等待结果"); Object result = guardedObject.getResult(500); log.info("结果为:{}", result); }, "t1").start(); new Thread(() -> { log.info("执行下载"); try { // 模拟下载资源 List<String> download = Downloader.download(); // 等待结果 guardedObject.setResult(download); } catch (IOException e) { e.printStackTrace(); } }, "t2").start(); }}class GuardedObject { // 结果变量 private Object result; // 获取结果,使用超时等待模式 public Object getResult(long timeout) { synchronized (this) { // 开始时间 long start = System.currentTimeMillis(); // 经历时间 long passedTime = 0; while (result == null) { // 记录还需要等待的时间 // 还需要等待的时间为timeout - passedTime(超时时间 - 已经等待的时间) long waitTime = timeout - passedTime; // 等待时间小于等于0,不需要继续等待,就跳出循环 if (waitTime <= 0) break; try { // 可能存在虚假唤醒, 避免过多等待,时间使用waitTime // 比如经过设定超时时间为2秒,1秒时传入空值,并唤醒了等待线程 // 这时发现,是虚假唤醒,并没有传入有效值,则需要继续等待 // 如果还设置为wait时间还是超时时间,那么还需要等待2秒,这里就会过多等待 // 正确的做法是,使用超时时间 - 已经等待的时间(2 - 1),计算出还需等待时间 // 这样在等待1秒就可以了,不会有贵多等待的情况 this.wait(waitTime); } catch (InterruptedException e) { e.printStackTrace(); } // 记录经历时间 passedTime = System.currentTimeMillis() - start; } } return result; } // 设置结果 public void setResult(Object result) { synchronized (this) { this.result = result; this.notifyAll(); } }}
2. 案例2(join方法源码)
public final synchronized void join(long millis) throws InterruptedException { // 开始等待时间 long base = System.currentTimeMillis(); // 经历时间 long now = 0;// 超时时间小于0,不合法 if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); }// 超时时间等于0,无限等待 if (millis == 0) { while (isAlive()) { wait(0); } // 超时时间大于0,进行超时等待模式 } else { // 确保线程存活 while (isAlive()) { // 设置等待时间 = 超时时间 - 已经经历时间 long delay = millis - now; // 等待时间小于等于0,等待结束,跳出循环 if (delay <= 0) { break; } // 等待时间 wait(delay); // 更新经历时间 now = System.currentTimeMillis() - base; } } }
jvm会在线程执行完 join() 方法后帮我们调用 notifyAll() 方法,这样主线程就会重回 Runnable 状态,时间片分配后主线程可执行 join() 方法之后的代码
5. 生产者消费者模式
public class Test10 { public static void main(String[] args) { MessageQueue queue = new MessageQueue(2); for (int i = 0; i < 3; i++) { int id = i; new Thread(() -> { queue.put(new Message(id, "消息:" + id)); }, "生产者" + i).start(); } new Thread(() -> { try { while (true) { Thread.sleep(1000); Message take = queue.take(); } } catch (InterruptedException e) { e.printStackTrace(); } }, "消费者").start(); }}// 消息队列类class MessageQueue { private LinkedList<Message> list = new LinkedList<>(); // 消息的队列集合 private int capcity; // 容量 public MessageQueue(int capcity) { this.capcity = capcity; } // 获取消息 public Message take() { // 检查队列是否为空 synchronized (list) { while (list.isEmpty()) { try { System.out.println("队列为空,消费者线程等待"); list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } Message message = list.removeFirst(); System.out.println(Thread.currentThread().getName() + " 已经消费消息:" + message); list.notifyAll(); return message; } } // 存入消息 public void put(Message message) { synchronized (list) { while (list.size() == capcity) { try { System.out.println("队列已满,生产者线程等待"); list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.addLast(message); System.out.println(Thread.currentThread().getName() + " 已经生产消息: " + message); list.notifyAll(); } }}// 消息类final class Message { private int id; private Object value; public Message(int id, Object value) { this.id = id; this.value = value; } public int getId() { return id; } public Object getValue() { return value; } @Override public String toString() { return "Message{" + "id=" + id + ", value=" + value + '}'; }}
执行结果:
九、park/unpark方法
1. 使用
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { log.info("t1- start..."); try { Thread.sleep(1000); // Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("t1 - park..."); LockSupport.park(); log.info("t1 - rerun..."); }, "t1"); t1.start(); Thread.sleep(2000); // Thread.sleep(1000); log.info("unpark"); LockSupport.unpark(t1); }
注:这里如果改为注释掉的两行代码,结果依然是t1线程会继续执行
与 Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
2. 原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter (干粮数), _cond (帐篷,等待补充干粮) 和 _mutex (互斥量)
- 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
- 调用 park 就是要看需不需要停下来歇息
- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需停留,继续前进
- 调用 unpark,就好比令干粮充足
- 如果这时线程还在帐篷,就唤醒让他继续前进
- 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
- 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮
1. 先调用park()
2. 再调用unpark()
3. 先调用unpark() 再调用 park()
十、活跃性
1. 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁;相互持有对方需要的锁,导致哪一个线程都无法继续和停止,就是死锁
Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A) {log.debug("lock A");sleep(1);synchronized (B) {log.debug("lock B");log.debug("操作...");}}}, "t1");Thread t2 = new Thread(() -> {synchronized (B) {log.debug("lock B");sleep(0.5);synchronized (A) {log.debug("lock A");log.debug("操作...");}}}, "t2");t1.start();t2.start();
2. 哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待
// 筷子类class Chopstick {String name;public Chopstick(String name) {this.name = name;}@Overridepublic String toString() {return "筷子{" + name + '}';}}
// 哲学家类class Philosopher extends Thread {Chopstick left;Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}private void eat() {log.debug("eating...");Sleeper.sleep(1);}@Overridepublic void run() {while (true) {// 获得左手筷子synchronized (left) {// 获得右手筷子synchronized (right) {// 吃饭eat();}// 放下右手筷子}// 放下左手筷子}}}
// 就餐Chopstick c1 = new Chopstick("1");Chopstick c2 = new Chopstick("2");Chopstick c3 = new Chopstick("3");Chopstick c4 = new Chopstick("4");Chopstick c5 = new Chopstick("5");new Philosopher("苏格拉底", c1, c2).start();new Philosopher("柏拉图", c2, c3).start();new Philosopher("亚里士多德", c3, c4).start();new Philosopher("赫拉克利特", c4, c5).start();new Philosopher("阿基米德", c5, c1).start();
3. 活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如
public class TestLiveLock {static volatile int count = 10;static final Object lock = new Object();public static void main(String[] args) {new Thread(() -> {// 期望减到 0 退出循环while (count > 0) {sleep(0.2);count--;log.debug("count: {}", count);}}, "t1").start();new Thread(() -> {// 期望超过 20 退出循环while (count < 20) {sleep(0.2);count++;log.debug("count: {}", count);}}, "t2").start();}}
4. 饥饿
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
十一、ReentrantLock可重入锁
与 synchronized 一样,都支持可重入
相对于 synchronized 它具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
基础语法:
// 获取锁reentrantLock.lock();try {// 临界区} finally {// 释放锁reentrantLock.unlock();}
API
1. 可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁;如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { lock.lock(); try { System.out.println("enter main"); m1(); } finally { lock.unlock(); } } public static void m1() { lock.lock(); try { System.out.println("enter m1"); m2(); } finally { lock.unlock(); } } private static void m2() { lock.lock(); try { System.out.println("enter m2"); } finally { lock.unlock(); } }
运行结果:
2. 可打断
使用API lock.lockInterruptibly();
获取可打断锁
private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { // 如果没有竞争那么此方法就会获取 lock 对象锁 // 如果有竞争就会进入阻塞队列,可以被其他线程用 interrupt 方法打断 System.out.println("尝试获得锁"); lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("没有获得锁"); return; } try { System.out.println("enter t1"); } finally { lock.unlock(); } }, "t1"); lock.lock(); t1.start(); Thread.sleep(1000); t1.interrupt(); }
运行结果
3. 锁超时
使用API lock.tryLock();
进行锁超时,获取不到锁立刻超时退出
private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("t1 尝试获取锁"); if (!lock.tryLock()) { System.out.println("t1 获取不到锁"); return; } try { System.out.println("t1 获得锁,执行临界区"); } finally { lock.unlock(); } }, "t1"); // 主线程先获取锁,t1线程获取锁失败 lock.lock(); System.out.println("主线程 获取锁"); t1.start(); }
运行结果:
使用API lock.tryLock(long timeout, TimeUnit unit);
进行锁超时,获取不到锁等待一段时间
private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("t1 尝试获取锁"); try { // 等待时间内尝试获取锁 if (!lock.tryLock(1,TimeUnit.SECONDS)) { System.out.println("t1 获取不到锁"); return; } } catch (InterruptedException e) { e.printStackTrace(); System.out.println("t1 被打断 获取不到锁"); return; } try { System.out.println("t1 获得锁,执行临界区"); } finally { lock.unlock(); } }, "t1"); // 主线程先获取锁,t1线程获取锁失败 lock.lock(); System.out.println("主线程 获取锁"); t1.start(); // 经过0.5秒主线程释放了锁,t1线程在时间范围内,可以获取锁 Thread.sleep(500); System.out.println("主线程 释放锁"); lock.unlock(); }
运行结果:
4. 公平锁
ReentrantLock 默认是不公平的
可以通过构造方法改为公平锁ReentrantLock lock = new ReentrantLock(true);
阻塞队列里面等待的线程会按先入先出的顺序,公平的执行
公平锁一般没有必要,会降低并发度
5.条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
基本语法:
private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { // 创建不同的休息室 waitSet Condition condition1 = lock.newCondition(); Condition condition2 = lock.newCondition(); lock.lock(); // 获取锁之后,可以让线程进入休息室等待 condition1.await(); condition1.await(1, TimeUnit.SECONDS); // 在休息室中,唤醒等待的线程 condition1.signal(); condition1.signalAll(); }
应用:
public class Test13 { private static ReentrantLock ROOM = new ReentrantLock(); static boolean hasCigarette = false; static boolean hasBreakfast = false; static Condition waitCigaretteCondition = ROOM.newCondition(); static Condition waitBreakfastCondition = ROOM.newCondition(); public static void main(String[] args) throws InterruptedException { new Thread(() -> { ROOM.lock(); try { log.info("没有烟,先休息"); while (!hasCigarette) { try { waitCigaretteCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.info("烟到了,开始干活"); } finally { ROOM.unlock(); } }, "A工人").start(); new Thread(() -> { ROOM.lock(); try { log.info("没有早饭,先睡觉"); while (!hasBreakfast) { try { waitBreakfastCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.info("早饭到了,开始吃"); } finally { ROOM.unlock(); } }, "B学生").start(); Thread.sleep(1000); // 设置两个线程,先后送烟和早饭 new Thread(() -> { ROOM.lock(); try { log.info("送烟的来了"); hasCigarette = true; waitCigaretteCondition.signal(); } finally { ROOM.unlock(); } }, "送烟的人").start(); Thread.sleep(500); // 设置两个线程,先后送烟和早饭 new Thread(() -> { ROOM.lock(); try { log.info("送早饭的来了"); hasBreakfast = true; waitBreakfastCondition.signal(); } finally { ROOM.unlock(); } }, "送早饭的人").start(); }}
运行结果:
6. 解决哲学家就餐问题
class Philosopher extends Thread {Chopstick left;Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}@Overridepublic void run() {while (true) {// 尝试获得左手筷子if (left.tryLock()) {try {// 尝试获得右手筷子if (right.tryLock()) {try {eat();} finally {// 吃完就会放下筷子right.unlock();}}} finally {// 关键:无论有没有获得右手筷子,最终都会放下左手筷子left.unlock();}}}}private void eat() {log.debug("eating...");Sleeper.sleep(1);}}
7. 运行顺序控制
固定运行顺序
要求先输出2,再输出1
1. wait \ notify
static final Object lock = new Object(); // 表示t2是否运行过 static boolean t2Runned = false; public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lock) { while (!t2Runned) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } log.info("1"); }, "t1"); Thread t2 = new Thread(() -> { synchronized (lock) { log.info("2"); t2Runned = true; lock.notifyAll(); } }, "t2"); t1.start(); t2.start(); }
2. park \ unpark
public static void main(String[] args) { Thread t1 = new Thread(() -> { LockSupport.park(); log.info("1"); }, "t1"); Thread t2 = new Thread(() -> { log.info("2"); LockSupport.unpark(t1); }, "t2"); t1.start(); t2.start(); }
交替运行顺序
线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现
1. wait \ notify
public class Test15 { public static void main(String[] args) { WaitNotify notify = new WaitNotify(1, 5); new Thread(() -> { notify.print(1, 2, "a"); }, "t1").start(); new Thread(() -> { notify.print(2, 3, "b"); }, "t2").start(); new Thread(() -> { notify.print(3, 1, "c"); }, "t3").start(); }}class WaitNotify { // 等待标记 private int flag; // 执行次数 private int loopNumber; public WaitNotify(int flag, int loopNumber) { this.flag = flag; this.loopNumber = loopNumber; } public void print(int waitFlag, int nextFlag, String msg) { for (int i = 0; i < loopNumber; i++) { synchronized (this) { while (flag != waitFlag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(msg+ " "); flag = nextFlag; this.notifyAll(); } } }}
2. await \ signal
public class Test16 { public static void main(String[] args) { AwaitSignal awaitSignal = new AwaitSignal(5); Condition conditionA = awaitSignal.newCondition(); Condition conditionB = awaitSignal.newCondition(); Condition conditionC = awaitSignal.newCondition(); new Thread(() -> { awaitSignal.print(conditionA, conditionB, "a"); }, "t1").start(); new Thread(() -> { awaitSignal.print(conditionB, conditionC, "b"); }, "t2").start(); new Thread(() -> { awaitSignal.print(conditionC, conditionA, "c"); }, "t3").start(); awaitSignal.startLock(conditionA); }}class AwaitSignal extends ReentrantLock { private int loopNumber; public AwaitSignal(int loopNumber) { this.loopNumber = loopNumber; } public void startLock(Condition condition) { lock(); try { condition.signal(); } finally { unlock(); } } public void print(Condition currentCondition, Condition nextCondition,String msg) { for (int i = 0; i < loopNumber; i++) { lock(); try { currentCondition.await(); System.out.print(msg + " "); nextCondition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { unlock(); } } }}
3. park \ unpark
public class Test17 { public static void main(String[] args) { ParkUnpark parkUnpark = new ParkUnpark(5); Thread t1 = new Thread(() -> { parkUnpark.print("a"); }, "t1"); Thread t2 = new Thread(() -> { parkUnpark.print("b"); }, "t2"); Thread t3 = new Thread(() -> { parkUnpark.print("c"); }, "t3"); parkUnpark.setThreads(t1, t2, t3); parkUnpark.startThread(); }}class ParkUnpark { private int loopNumber; private Thread[] threads; public ParkUnpark(int loopNumber) { this.loopNumber = loopNumber; } public void setThreads(Thread... threads) { this.threads = threads; } public void print(String msg) { for (int i = 0; i < loopNumber; i++) { LockSupport.park(); System.out.print(msg + " "); LockSupport.unpark(nextThread()); } } private Thread nextThread() { int index = 0; Thread currentThread = Thread.currentThread(); for (int i = 0; i < threads.length; i++) { if (threads[i] == currentThread) { index = i; break; } } if (index < threads.length - 1) return threads[index + 1]; else return threads[0]; } public void startThread() { for (Thread thread : threads) { thread.start(); } LockSupport.unpark(threads[0]); }}