巅峰网站建设,如何做闲置物品自己的网站,怎么建设国外免费网站,互联网站点黑马课程 文章目录1. Java线程1.1 创建和运行线程方法一#xff1a;Thread方法二#xff1a;Runnable#xff08;推荐#xff09;lambda精简Thread和runnable原理方法三#xff1a;FutureTask配合Thread1.2 查看进程和线程的方法1.3 线程运行原理栈与栈帧线程上下文切换1.… 黑马课程 文章目录1. Java线程1.1 创建和运行线程方法一Thread方法二Runnable推荐lambda精简Thread和runnable原理方法三FutureTask配合Thread1.2 查看进程和线程的方法1.3 线程运行原理栈与栈帧线程上下文切换1.4 线程常见方法方法概述start() 和 run()sleep() 和 yield()join()interrupt()过时方法主线程和守护线程1.5 终止模式之两阶段终止模式1.6 应用 - 防止CPU占用100%sleep1.7 习题烧水泡茶多线程方案1.8 小结2. 并发共享模型之管程 (悲观锁)2.1 synchronized 解决方案面向过程改进面向对象方法上的synchronized习题线程八锁2.2 线程安全分析成员变量的线程不安全局部变量是线程安全的局部变量的线程不安全2.3 常见线程安全类2.4 习题线程安全性判断练习卖票*练习转账2.5 MonitorJava对象头Monitor锁synchronized原理synchronized优化多种锁轻量级锁锁膨胀自旋优化偏向锁撤销偏向锁批量重偏向和批量撤销锁消除3. 同步3.1 wait notify3.2 同步模式之保护性暂停Guarded Suspensionjoin 原理多任务版 Guard Suspension3.3 同步模式之生产者/消费者3.4 pack和unpack3.5 线程状态3.6 线程状态的转换1. Java线程
前期准备 导入依赖
dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId
/dependency
dependencygroupIdch.qos.logback/groupIdartifactIdlogback-classic/artifactId
/dependency1.1 创建和运行线程
方法一Thread
Slf4j(topic test)
public class ConcurrentApplication {public static void main(String[] args) {Thread t new Thread(){Overridepublic void run() {log.debug(running inside);}};t.setName(t1);t.start();log.debug(running outside);}
}方法二Runnable推荐
把【线程】和【任务】要执行的代码分开
Thread 代表线程Runnable 可运行的任务线程要执行的代码
Slf4j(topic test)
public class ConcurrentApplication {public static void main(String[] args) {Runnable runnable new Runnable() {Overridepublic void run() {log.debug(running inside);}};Thread t new Thread(runnable);t.setName(t1);t.start();log.debug(running outside);}}lambda精简
runnable是一个函数式接口可以用lambda简化
FunctionalInterface
public interface Runnable {public abstract void run();
}如下
Slf4j(topic test)
public class ConcurrentApplication {public static void main(String[] args) {Runnable runnable () - log.debug(running inside);Thread t new Thread(runnable);t.setName(t1);t.start();log.debug(running outside);}
}Thread和runnable原理
class Thread implements Runnable{//1. runnable作为参数传递到Thread的构造方法中然后交由init函数public Thread(Runnable target) {init(null, target, Thread- nextThreadNum(), 0);}//2. init函数将调用它的重载函数private void init(ThreadGroup g, Runnable target, String name, long stackSize) {init(g, target, name, stackSize, null, true);}//3. 重载函数将target赋值给Thread的私有变量targetprivate void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {...this.target target;...}//4. 根据私有变量target是否为空选择是否执行target方法Overridepublic void run() {if (target ! null) {target.run();}}
}无论是否有runnable走的都是Thread自身的run方法方法一是重写Thread的run方法方法二是通过Thread的run方法执行传来的Runnable对象里的run方法
方法三FutureTask配合Thread
FutureTask 能够接收 Callable 类型的参数用来处理有返回结果的情况
Slf4j(topic test)
public class ConcurrentApplication {public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTaskInteger task new FutureTask(new CallableInteger() {Overridepublic Integer call() throws Exception {log.debug(running);Thread.sleep(2000);return 100;}});Thread t new Thread(task);t.setName(t1);t.start();//等待结果返回log.debug({}, task.get());//{}是占位符}
}1.2 查看进程和线程的方法 windows tasklist 查看进程
tasklist | findstr keyword 根据关键字查找进程
taskkill /F /PID PID 根据进程号杀死进程linux ps -fe 查看所有进程
ps -fe | grep keyword 根据关键字查找
kill PID 杀死进程
top 以动态方式展示进程
top -H -p PID 根据进程号查找线程java jsp 查看所有Java进程
jstack PID 查看某个Java进程的所有线程情况
jconsole 查看某个Java进程中线程的运行情况图形界面jconsole有兴趣学习
1.3 线程运行原理
栈与栈帧
JVM 由堆、栈、方法区所组成其中栈内存就是给线程使用的每个线程启动后虚拟机就会为其分配一块栈内存
每个栈由多个栈帧Frame组成对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧对应着当前正在执行的那个方法
示例程序
public class ConcurrentApplication {public static void main(String[] args) throws ExecutionException, InterruptedException {method1(10);}private static void method1(int x){int y x1;Object m method2();System.out.println(m);}private static Object method2(){Object n new Object();return n;}
}栈帧 jvm加载 ConcurrentApplication 类到方法区启动一个名为 main 的主线程并为其分配栈内存由多个栈帧组成将main线程交给任务调度器调度执行main栈帧、method1栈帧、method2栈帧依次进入mian线程栈每个线程中有一个程序计数器记录下一条执行命令 每个线程一个栈线程中的每一个方法为一个栈帧 多线程debug时模式要选线程Thread 线程上下文切换
可能的原因
线程的 cpu 时间片用完垃圾回收有更高优先级的线程需要运行线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当Context Switch时需要由操作系统保存当前线程的状态 Java使用程序计数器记录下一条 jvm 指令的执行地址是线程私有的
状态包括程序计数器、虚拟机栈中每个栈帧的信息如局部变量、操作数栈、返回地址等Context Switch 频繁发生会影响性能
1.4 线程常见方法
方法概述 start() 启动一个新线程在新的线程中运行 run 方法中的代码start 方法只是让线程进入就绪里面代码不一定立刻运行每个线程对象的start方法只能调用一次如果调用了多次会出现IllegalThreadStateException run() 新线程启动后会调用的方法 join() 等待线程运行结束 join(long n) 等待线程运行结束,最多等待 n毫秒 getId() 获取线程长整型的 id getName() setName(String) getPriority() setPriority(int) java中规定线程优先级是1~10 的整数较大的优先级能提高该线程被 CPU 调度的机率 public final static int MIN_PRIORITY 1;//最小
public final static int NORM_PRIORITY 5;//默认
public final static int MAX_PRIORITY 10;//最大getState() 获取线程状态Java 中线程状态是用 6 个 enum 表示分别为NEW, RUNNABLE, BLOCKED, WAITING,TIMED_WAITING, TERMINATED isInterrupted() 判断是否被打断不影响打断标志 isAlive() 线程是否存活是否运行完毕 interrupt() 打断线程如果被打断线程正在 sleepwaitjoin 会导致被打断的线程抛出 InterruptedException并清除打断标记如果打断的正在运行的线程则会设置 打断标记park 的线程被打断也会设置打断标记 interrupted() static判断当前线程是否被打断会清除打断标志 currentThread() static获取当前正在执行的线程 sleep(long n) static让当前执行的线程休眠n毫秒休眠时让出 cpu 的时间片给其它线程 yield() static提示线程调度器让出当前线程对CPU的使用主要是为了测试和调试
start() 和 run() run Slf4j(topic test)
public static void main(String[] args) throws ExecutionException, InterruptedException {Thread t1 new Thread(t1){Overridepublic void run(){log.debug(running inside);}};t1.run();
}执行结果执行run方法的是 main 线程新创建的线程并未启动 start //查看启动前后线程的状态
System.out.println(t1.getState());//状态NEW
t1.start();
System.out.println(t1.getState());//状态RUNNABLEsleep() 和 yield() sleep 调用 sleep 会让当前线程从 Runnable 进入 Timed Waiting 状态阻塞 其它线程可以使用 interrupt 方法打断正在睡眠的线程这时 sleep 方法会抛出 InterruptedException t1.interrupt();//叫醒t1线程睡眠结束后的线程未必会立刻得到执行可能cpu正忙 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 TimeUnit.SECONDS.sleep(2);//睡眠2秒yield 调用 yield 会让当前线程从 Running运行状态 进入 Ready就绪状态然后调度执行其它线程 Runnable包括 Running(运行) 和 Ready(就绪) 2种状态具体的实现依赖于操作系统的任务调度器 sleep会使当前线程陷入阻塞而yield不会阻塞只是让出cpu资源而已 join()
public static void main(String[] args){...t1.start();...t1.join();//等待线程t1执行完毕之后再执行main中后面的代码...
}join(n)有时限的等待
interrupt()
对于正常运行的线程interrupt不会影响其运行只是会设置打断标记为true对于处于sleep等的线程interrupt会将其唤醒即打断阻塞
打断标记如果本线程被打断过打断标记将为true 打断 sleepwaitjoin 的线程会清除打断标记仍为false Thread t1 new Thread(() -{try{ Thread.sleep(10000);} catch (InterruptedException e) { e.printStackTrace(); }try{ Thread.sleep(10000);} catch (InterruptedException e) { e.printStackTrace(); }
}, t1);t1.start();
Thread.sleep(1000); //等t1进入sleep
log.debug(before interrupt: {}, t1.isInterrupted());
log.debug(before interrupt: {}, t1.getState());t1.interrupt();
Thread.sleep(1000); //等t1再次进入sleep
log.debug(after interrupt: {}, t1.isInterrupted());
log.debug(after interrupt: {}, t1.getState());结果 01:29:13.546 [main] DEBUG test - before interrupt: false
01:29:13.551 [main] DEBUG test - before interrupt: TIMED_WAITING
java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at com.example.ConcurrentApplication.lambda$main$0(ConcurrentApplication.java:18)at java.lang.Thread.run(Thread.java:748)
01:29:14.565 [main] DEBUG test - after interrupt: false
01:29:14.565 [main] DEBUG test - after interrupt: TIMED_WAITING注意这里 interrupt 之后打断标记会短暂地标记为true然后再被标记为false 可以通过去掉main主线程的第二次sleep观察到 打断正常运行的线程不会清除打断标记变为true Thread t1 new Thread(() -{while(true){boolean interrupted Thread.currentThread().isInterrupted();if(interrupted){log.debug(被打断了退出循环);break;}}
}, t1);
t1.start();
Thread.sleep(1000);
t1.interrupt();可以用来停止线程 打断park线程不会清除打断标记 初始打断标记为false打断后标记为true注意打断标记为true时park将失效解决方法使用 interrupted() 方法
过时方法
不推荐使用的方法这些方法已过时容易破坏同步代码块造成线程死锁 stop()停止线程运行 废弃原因方法粗暴除非可能执行 finally 代码块以及释放 synchronized 外线程将直接被终止如果线程持有 JUC 的互斥锁可能导致锁来不及释放造成其他线程永远等待的局面 suspend()挂起暂停线程运行 废弃原因如果目标线程在暂停时对系统资源持有锁则在目标线程恢复之前没有线程可以访问该资源如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源则会导致死锁 resume()恢复线程运行
主线程和守护线程
默认情况下Java 进程需要等待所有线程都运行结束才会结束 有一种特殊的线程叫做守护线程只要其它非守护线程运行结束了即使守护线程的代码没有执行完也会强制结束
Thread t1 new Thread(()-{while(true){if(Thread.currentThread().isInterrupted()){break;}}log.debug(未运行的部分);
}, t1);
t1.setDaemon(true);//设置为守护线程
t1.start();
log.debug(finish);结果即便t1线程是一个while循环也可观察到java进程的结束
垃圾回收器线程就是一种守护线程Tomcat 中的 Acceptor 和 Poller 线程都是守护线程所以 Tomcat 接收到 shutdown 命令后不会等待它们处理完当前请求
1.5 终止模式之两阶段终止模式
错误思路
使用线程对象的 stop() 方法停止线程 stop 方法会真正杀死线程如果这时线程锁住了共享资源那么当它被杀死后就再也没有机会释放锁其它线程将永远无法获取锁 使用 System.exit(int) 方法停止线程 目的仅是停止一个线程但这种做法会让整个程序都停止
应用示例
需求每隔一段时间打印监控数据 package com.example;Slf4j(topic c.test)
public class ConcurrentApplication {public static void main(String[] args) throws InterruptedException {TwoPhaseTermination tpt new TwoPhaseTermination();tpt.start();Thread.sleep(3500);tpt.stop();}
}Slf4j(topic c.TwoPhaseTermination)
class TwoPhaseTermination {private Thread monitor;//启动监控线程public void start(){monitor new Thread(()-{while(true){Thread current Thread.currentThread();//如果被打断了if(current.isInterrupted()){log.debug(料理后事);break;}//未被打断无异常则每隔1秒执行一次监控记录try {Thread.sleep(1000);//如果在这里sleep被打断将进入catch里面log.debug(执行监控记录);}catch (InterruptedException e){e.printStackTrace();current.interrupt();//重新设置打断标记为true应对sleep时打断情况}}});monitor.start();}//停止监控线程public void stop(){monitor.interrupt();}
}结果优雅结束线程
01:57:22.026 [Thread-1] DEBUG c.TwoPhaseTermination - 执行监控记录
01:57:23.034 [Thread-1] DEBUG c.TwoPhaseTermination - 执行监控记录
01:57:24.036 [Thread-1] DEBUG c.TwoPhaseTermination - 执行监控记录
java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at com.example.TwoPhaseTermination.lambda$start$0(ConcurrentApplication.java:40)at java.lang.Thread.run(Thread.java:748)
01:57:24.533 [Thread-1] DEBUG c.TwoPhaseTermination - 料理后事1.6 应用 - 防止CPU占用100%sleep 在一个1核虚拟机上实验 public class ConcurrentApplication {public static void main(String[] args){new Thread(()-{while (true){//如果不加下面一句cpu会占满至100%try{ Thread.sleep(1); }catch (Exception e){}}}).start();}
} 可以用 wait 或 条件变量达到类似的效果不同的是后两种都需要加锁并且需要相应的唤醒操作一般适用于要进行同步的场景sleep 适用于无需锁同步的场景
1.7 习题烧水泡茶多线程方案
题目
想泡壶茶喝。情况是开水没有水壶要洗茶壶、茶杯要洗火已生了茶叶也有了。怎么办
分析 实现
public static void sleep(int i){try{TimeUnit.SECONDS.sleep(i);}catch (InterruptedException e){e.printStackTrace();}
}
public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {log.debug(洗水壶);sleep(1);log.debug(烧开水);sleep(15);}, zhangsan);Thread t2 new Thread(() - {log.debug(洗茶壶);sleep(1);log.debug(洗茶杯);sleep(2);log.debug(拿茶叶);sleep(1);try {t1.join();//等待开水烧好} catch (InterruptedException e) {e.printStackTrace();}log.debug(泡茶);}, lisi);t1.start();t2.start();
}改进之处
需要zhangsan来最后泡茶目前两个线程是各执行各的如果需要交换信息呢
1.8 小结
本章的重点在于掌握
线程创建线程重要 api如 startrunsleepjoininterrupt 等应用方面 异步调用主线程执行期间其它线程异步执行耗时操作提高效率并行计算缩短运算时间同步等待join统筹规划合理使用线程得到最优效果 原理方面 线程运行流程栈、栈帧、上下文切换、程序计数器Thread 两种创建方式 的源码 模式方面 终止模式之两阶段终止
2. 并发共享模型之管程 (悲观锁) Monitor称为 管程、监视器是重量级锁的原理 悲观锁阻塞等待 思考两个线程对初始值为0的静态变量做自增和自减各执行5000最后结果是多少 答案可能为0可能为正可能为负 分析自增实际上会产生如下的JVM字节码命令自减类似 getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i变量存储在主内存中自增自减需要将变量的值读取到自己线程独有的工作内存中操作当自增自减同时读取了 i 的值那么最终写入时会有一方覆盖另一方的结果导致某方本次操作失效从而使得结果变化难定 竞态条件
Race Condition多个线程在临界区内执行由于代码的执行序列不同而导致结果无法预测称之为发生了竞态条件
解决方案
阻塞式synchronized对象锁Lock非阻塞式原子变量
2.1 synchronized 解决方案 java 中互斥和同步都可以采用 synchronized 关键字来完成 synchronized俗称 对象锁 面向过程
语法
synchronized(对象){临界区
}示例2个线程做自增自减 锁推荐使用 final static int counter 0; //静态变量
static Object lock new Object(); //锁public static void main(String[] args){Thread t1 new Thread(() - {for(int i0; i5000; i){synchronized (lock){counter;}}}, t1);Thread t2 new Thread(() - {for(int i0; i5000; i){synchronized (lock){counter--;}}}, t2);t1.start();t2.start();System.out.println(counter);
}synchronized 实际是用对象锁保证了临界区内代码的原子性临界区内的代码对外是不可分割的不会被线程切换所打断
改进面向对象
Slf4j(topic c.test)
public class ConcurrentApplication {public static void main(String[] args){Room room new Room();Thread t1 new Thread(() - {for(int i0; i5000; i) room.increment();}, t1);Thread t2 new Thread(() - {for(int i0; i5000; i) room.decrement();}, t2);t1.start();t2.start();System.out.println(room.getCounter());}
}class Room{private int counter 0;public void increment(){synchronized (this){ //这里的this指的是调用该方法的对象即锁对象counter;}}public void decrement(){synchronized (this){counter--;}}public int getCounter(){synchronized (this){ //使读取过程中counter不会被修改return counter;}}
}关于this 当使用锁的时候必然需要创建一个锁对象。例如Room room new Room(); 这里的this就是指代调用该方法的Room对象即room 方法上的synchronized 非静态方法 class Test{public synchronized void test(){}
}
//等价于
class Test{public void test(){synchronized (this){};}
}synchronized (this)锁的是该方法所在类的实例对象 静态方法 class Test{public synchronized static void test(){}
}
//等价于
class Test{public static void test(){synchronized (Test.class){};}
}前面的Room就可以简化为
class Room {private int counter 0;public synchronized void increment() {counter;}public synchronized void decrement() {counter--;}public synchronized int getCounter() {return counter;}
}习题线程八锁 考察 synchronized 锁住的是哪个对象 锁对象n1多个线程是同一个锁对象 题1 public static void main(String[] args) {Number n1 new Number();new Thread(()-{ n1.a(); }).start();new Thread(()-{ n1.b(); }).start();
}
Slf4j(topic c.Number)
class Number{public synchronized void a() { log.debug(1); }public synchronized void b() { log.debug(2); }
}结果12或2112概率大因为线程1先启动 题2 public static void main(String[] args) {Number n1 new Number();new Thread(()-{ n1.a(); }).start();new Thread(()-{ n1.b(); }).start();
}
Slf4j(topic c.Number)
class Number{public synchronized void a() { sleep(1);//这里的sleep被封装过代表1秒log.debug(1); }public synchronized void b() { log.debug(2); }
}锁对象n1 结果 如果是t1先获得调度那么结果1s 后打印 12如果是t2先获得调度那么结果立即打印 21s 后打印 1 题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();
}
Slf4j(topic c.Number)
class Number{public synchronized void a() {sleep(1);log.debug(1);}public synchronized void b() {log.debug(2);}public void c() {log.debug(3);}
}结果 t1先获得调度立即打印 31s 后打印 12312t2先获得调度立即打印 231s 后打印 1 231t3先获得调度立即打印312看调度顺序312321 题4 public static void main(String[] args) {Number n1 new Number();Number n2 new Number();new Thread(()-{ n1.a(); }).start();new Thread(()-{ n2.b(); }).start();
}
Slf4j(topic c.Number)
class Number{public synchronized void a() {sleep(1);log.debug(1);}public synchronized void b() {log.debug(2);}
}结果21相当于未加锁不存在互斥 题5 public static void main(String[] args) {Number n1 new Number();new Thread(()-{ n1.a(); }).start();new Thread(()-{ n1.b(); }).start();
}
Slf4j(topic c.Number)
class Number{public static synchronized void a() {sleep(1);log.debug(1);}public synchronized void b() {log.debug(2);}
}结果a() 锁住的是类对象 Number.classb() 锁住的是普通对象 n1两个锁对象不同相当于未加锁输出 21 题6 public static void main(String[] args) {Number n1 new Number();new Thread(()-{ n1.a(); }).start();new Thread(()-{ n1.b(); }).start();
}
Slf4j(topic c.Number)
class Number{public static synchronized void a() {sleep(1);log.debug(1);}public static synchronized void b() {log.debug(2);}
}结果21或12锁住了同一个类对象存在互斥 题7 public static void main(String[] args) {Number n1 new Number();Number n2 new Number();new Thread(()-{ n1.a(); }).start();new Thread(()-{ n2.b(); }).start();
}
Slf4j(topic c.Number)
class Number{public static synchronized void a() {sleep(1);log.debug(1);}public synchronized void b() {log.debug(2);}
}结果21相当于未加锁不存在互斥 题8 public static void main(String[] args) {Number n1 new Number();Number n2 new Number();new Thread(()-{ n1.a(); }).start();new Thread(()-{ n2.b(); }).start();
}
Slf4j(topic c.Number)
class Number{public static synchronized void a() {sleep(1);log.debug(1);}public static synchronized void b() {log.debug(2);}
}结果12 或 21是同一个类对象锁存在互斥
2.2 线程安全分析
成员变量和静态变量是否线程安全
如果它们没有共享则线程安全如果它们被共享了根据它们的状态是否能够改变又分两种情况 如果只有读操作则线程安全如果有读写操作则这段代码是临界区需要考虑线程安全
局部变量是否线程安全
局部变量是线程安全的——存储在每个栈帧中并不共享但局部变量引用的对象则未必 如果该对象没有逃离方法的作用访问它是线程安全的如果该对象逃离方法的作用范围需要考虑线程安全
成员变量的线程不安全
static final int THREAD_NUMBER 2;
static final int LOOP_NUMBER 200;
public static void main(String[] args) {ThreadUnsafe test new ThreadUnsafe();for (int i 0; i THREAD_NUMBER; i) {new Thread(() - {test.method1(LOOP_NUMBER);}, Thread i).start();}
}class ThreadUnsafe {ArrayListString list new ArrayList();public void method1(int loopNumber) {for (int i 0; i loopNumber; i) {method2();method3();}}private void method2() {list.add(1);}private void method3() {list.remove(0);//删除第1个元素}
}原因这里的 test对象 和 list对象 是线程共享的 分析 看似每一次remove前都add过一次似乎不会出现错误。但是考虑以下情况 两个线程都执行add操作由于读取时恰好读取了同一个index所以出现一次add被覆盖掉了两个都添加在了同一个index上 此时相当于只增加了一个数据却要删除2个数据因此报错 IndexOutOfBoundsException
局部变量是线程安全的
static final int THREAD_NUMBER 2;
static final int LOOP_NUMBER 200;
public static void main(String[] args) {ThreadSafe test new ThreadSafe();for (int i 0; i THREAD_NUMBER; i) {new Thread(() - {test.method1(LOOP_NUMBER);}, Thread i).start();}
}class ThreadSafe {public void method1(int loopNumber) {ArrayListString list new ArrayList();for (int i 0; i loopNumber; i) {method2(list);method3(list);}}private void method2(ArrayListString list) {list.add(1);}private void method3(ArrayListString list) {list.remove(0);}
}每次调用 method1 都会新创建一个 list 实例相当于两个线程利用同一个 test 对象创建了两个不同的 list 对象在堆上互不影响 思考如果这里的method2和method3改为public被其他线程调用还是线程安全的吗 答案是线程安全的。即便供其他线程调用其他线程传来的也是该线程的list不会影响到本线程的list 局部变量的线程不安全
static final int THREAD_NUMBER 2;
static final int LOOP_NUMBER 200;
public static void main(String[] args) {ThreadSafeSubClass test new ThreadSafeSubClass();for (int i 0; i THREAD_NUMBER; i) {new Thread(() - {test.method1(LOOP_NUMBER);}, Thread i).start();}
}class ThreadSafe {public void method1(int loopNumber) {ArrayListString list new ArrayList();for (int i 0; i loopNumber; i) {method2(list);method3(list);}}public void method2(ArrayListString list) {list.add(1);}public void method3(ArrayListString list) {list.remove(0);}
}
class ThreadSafeSubClass extends ThreadSafe{Overridepublic void method3(ArrayListString list) {new Thread(() - {list.remove(0);}).start();}
}这里是会出现线程不安全的父子线程将共用一个list 一种不安全的情况如下 ThreadSafe里面的for循环2次那么就有1个父线程执行2次add2个子线程各执行1次remove这3个线程共享同一个list 执行顺序如果是第1个add完成size1 —— 第1个remove尚未完成size1 —— 第2个add完成size2—— 第1个remove完成size0 —— 第2个remove报错 这里也可以看出private和final对线程安全的意义 不以父子类来看概括的说只要出现共享变量就会存在线程不安全的问题 2.3 常见线程安全类
StringIntegerStringBufferRandomVectorHashtablejava.util.concurrent 包下的类
这里说它们是线程安全的是指多个线程调用它们同一个实例的某个方法时是线程安全的如下
Hashtable table new Hashtable();
new Thread(()-{table.put(key1, value1);
}).start();
new Thread(()-{table.put(key2, value2);
}).start();HashTable中的put方法定义如下 public synchronized V get(Object key){}但注意它们多个方法的组合不是原子的例如下述代码就不是原子的
Hashtable table new Hashtable();
if(table.get(key) null){table.put(key, value);
}
//get和put单独都是线程安全的但它们组合使用是不安全的String、Integer 等都是不可变类因为其内部的状态不可以改变因此它们的方法都是线程安全的 String的subString是返回一个新的String对象不会改变原有字符串 2.4 习题
线程安全性判断
例1
public class MyServlet extends HttpServlet {MapString,Object map new HashMap();//不安全String S1 ...;//安全final String S2 ...;//安全Date D1 new Date();//不安全final Date D2 new Date();//不安全final规定了D2的引用值不能改变但对象里面的属性是可以改变的
}例2
//MyServlet只有一份对应的UserServiceImpl只有一份所以这里是线程不安全的
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 int count 0;public void update() {count;}
}例3
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));}
}MyAspect默认应该是单例模式单例bean被所有线程共享start作为成员变量也将被线程共享 因此上面代码是线程不安全的 bean中最好不要使用成员变量改为环绕通知使用局部变量 例4
public class MyServlet extends HttpServlet {// 是否安全—— 线程安全private UserService userService new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}
public class UserServiceImpl implements UserService {// 是否安全 —— 线程安全userDao里面没有可更改的属性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 ?;// 是否安全—— 线程安全没有成员变量的类大多线程安全这里的conn创建在各自的线程空间之中try (Connection conn DriverManager.getConnection(,,)){// ...} catch (Exception e) {// ...}}
}例5
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() {String sql update user set password ? where username ?;conn DriverManager.getConnection(,,);// ...}
}例6
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() {private UserDao userDao new UserDaoImpl();userDao.update();}
}
public class UserDaoImpl implements UserDao {// 是否安全—— 线程不安全private Connection conn null;public void update() {String sql update user set password ? where username ?;conn DriverManager.getConnection(,,);// ...}
}例7
public abstract class Test {public void bar() {// 是否安全—— 线程不安全如果foo被子类继承且子类有新的线程那么父子类共享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 的行为是不确定的可能导致不安全的发生被称之为外星方法 思考为什么String类要设置为final —— 保证了线程安全 练习卖票
思考下列代码是否存在线程安全性问题如果存在如何改正
package com.example;Slf4j(topic c.test)
public class ConcurrentApplication {public static void main(String[] args) throws InterruptedException {//模拟多人买票TicketWindow window new TicketWindow(10000);//卖出的票数统计ListInteger amountList new Vector();//Vector是线程安全的List不是//为了使主线程在所有抢票线程结束之后再统计余票需要join所有抢票线程//可以使用一个List来循环join操作ListThread threadList new ArrayList();//假设2000个人在抢票for(int i0; i2000; i){//每个人随机买1-5张票Thread thread new Thread(() - {int amount window.sell(randomAmount());amountList.add(amount);});threadList.add(thread);thread.start();}//等待2000个抢票线程执行完毕for(Thread thread : threadList){thread.join();}//验证是否线程安全卖出的票数剩余的票数总票数log.debug(余票{}, window.getCount());log.debug(卖出的票数{}, amountList.stream().mapToInt(i - i).sum());}//随机1-5static Random random new Random();public static int randomAmount(){return random.nextInt(5)1;}
}class TicketWindow{private int count;public TicketWindow(int count){this.count count;}public int getCount(){return this.count;}public int sell(int amount){if(this.count amount){this.count - amount;return amount;}else return 0;}
}某次结果如下 这里余票卖出的票数大于总票数显然是有问题的主要在于TicketWindow.sell()方法它是线程不安全的
分析存在读写的地方
int amount window.sell(randomAmount());//不安全
amountList.add(amount);//安全Vector的add自身已经被定义为了synchronized不用再考虑
threadList.add(thread);//安全ArrayList虽然不是线程安全类但由于该语句只在主线程中使用不存在线程共享改进方法
public synchronized int sell(int amount){if(this.count amount){this.count - amount;return amount;}else return 0;
}*练习转账
思考下列代码是否存在线程安全性问题如果存在如何改正
package com.example;Slf4j(topic c.test)
public class ConcurrentApplication {public static void main(String[] args) throws InterruptedException {Account a new Account(1000);Account b new Account(1000);//a不断向b转账Thread t1 new Thread(() - {for(int i0; i1000; i){a.transfer(b, randomAmount());}}, t1);//同时b也不断向a转账Thread t2 new Thread(() - {for(int i0; i1000; i){b.transfer(a, randomAmount());}}, t2);t1.start();t2.start();t1.join();t2.join();//验证是否有错误a账户b账户 2000log.debug(total: {}, (a.getMoney()b.getMoney()));}//随机1-5static Random random new Random();public static int randomAmount(){return random.nextInt(5)1;}
}//账户
class Account{private int money;public Account(int money){this.moneymoney;}public int getMoney() {return money;}public void setMoney(int money) {this.money money;}//转账public void transfer(Account target, int amount){if(this.money amount){this.setMoney(this.getMoney()-amount);target.setMoney(target.getMoney()amount);}}
}结果多次运行后可以看到有的时候 total 2000 total 2000 total 2000 这3种情况都有出现
改进方法 注意这里改进不对的话还会造成死锁 错误方法 public synchronized void transfer(Account target, int amount){if(this.money amount){this.setMoney(this.getMoney()-amount);target.setMoney(target.getMoney()amount);}
}分析它相当于 public synchronized void transfer(Account target, int amount){synchronized(this){if(this.money amount){this.setMoney(this.getMoney()-amount);target.setMoney(target.getMoney()amount);}}
}当a向b转账时这里的this指的是a的账户也就是说 a.transfer(b, randomAmount())是安全的但b.transfer(a, randomAmount())是不安全的因为a上锁了但b没有 反之亦然在 transfer上加 synchronized只能保证单向转账不能双方同时转账 分析假设ab同时转账a–b 10b --a 20其中一种情况可能为 线程B先开始b调用transfer此时b账户上锁this.setMoneyb980target.setMoney读取但尚未写入a1000 线程Aa调用transfer此时账户a上锁this.setMoneya990等待b的锁 线程Btarget.setMoney继续写入a1020覆盖掉线程A对账户a的操作此时线程B结束释放b的锁 线程Atarget.setMoneyb990 最终结果a1020b990 如果线程A先开始有可能出现 a1010b1010 的情况 错误方法 public synchronized void setMoney(int money) {this.money money;}
//转账
public synchronized void transfer(Account target, int amount){if(this.money amount){this.setMoney(this.getMoney()-amount);target.setMoney(target.getMoney()amount);}
}分析容易导致死锁问题 线程Aa.transfer对a账户加锁this.setMoney对setMoney加锁准备调用target.setMoney线程Bb.transfer对b账户加锁调用this.setMoney发现它已被线程A加锁于是等待线程A释放setMoney的锁线程A调用target.setMoney发现线程B已对b账户加锁于是等待线程B释放b账户的锁线程AB都在等待对方释放锁最终陷入死锁 可行方法 //转账
public void transfer(Account target, int amount){synchronized (Account.class){if(this.money amount){this.setMoney(this.getMoney()-amount);target.setMoney(target.getMoney()amount);}}
}这只是临时解决实际上是不会采用这种方式的因为效率非常的慢同一时间只允许一个人操作
2.5 Monitor 这一节有些知识点比较模糊可能存在错误之处待深入学习改正 Java对象头
以32位虚拟机为例 int 类型占 4 字节 Integer 类型占 16 字节4字节数据 8字节对象头 4字节的对齐 Mark Word 32位系统 64位系统 hashcode哈希码age分代年龄biased_lock是不是偏向锁 不同状态下Mark Word的结构会变化 普通对象的对象头 一个普通对象的对象头占 64 bits即8字节Mark Word 占 32 bits对象的基本信息Klass Word 占 32 bits指针指向这个对象对应的Class 数组对象的对象头 一个数组对象的对象头占 96 bits即12字节Mark Word 占 32 bitsKlass Word 占 32 bitsarray length 占 32 bits
Monitor锁
Monitor常称为 监视器 或 管程是对象锁的底层原理
每个Java对象都可以关联一个 Monitor 对象如果使用 synchronized 给对象上锁重量级之后该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针 对象是java提供的Monitor是操作系统提供的上锁时对象的 Mark Word 标志变为 10Heavyweight Locked剩下的 30 bits 指针指向Monitor对象Owner当前锁的拥有者EntryList等待队列等待该锁被释放的线程队列这些线程处于阻塞状态WaitSet线程队列这些线程该之前获得过锁但条件不满足从而进入 WAITING 状态的线程后面会解释
synchronized原理
public class ConcurrentApplication {static final Object lock new Object();static int counter 0;public static void main(String[] args){synchronized (lock){counter;}}
}对应的字节码文件 打印字节码 javac ConcurrentApplication.java javap -c ConcurrentApplication.class public class ConcurrentApplication {static final java.lang.Object lock;static int counter;...public static void main(java.lang.String[]);Code:0: getstatic #2 // 拿到lock引用synchronized的开始3: dup // 复制了一份lock引用4: astore_1 // 将复制的lock引用存放在 slot 1 里面5: monitorenter // 将lock对象的 Mark Word 置为 Monitor 指针6: getstatic #3 // 这里开始4句做 counter 操作9: iconst_110: iadd11: putstatic #314: aload_1 // 即将离开临界区此时先从 slot 1 拿到之前存储的lock引用15: monitorexit // 将lock对象 Mark Word 重置原信息保存在monitor中唤醒 EntryList16: goto 24 // 跳转到24行结束执行19: astore_2 // 从这里开始处理异常情况将异常对象e存储到 slot 2中20: aload_1 // 出现异常以至于未能释放锁此时也能获取到锁21: monitorexit // 将lock对象 Mark Word 重置原信息保存在monitor中唤醒 EntryList22: aload_2 // 获取到异常对象e23: athrow // 抛出异常 throw e24: returnException table: // 监控6到16行即synchronized部分如果出现异常跳转到19行from to target type6 16 19 any19 22 19 any...
}synchronized优化多种锁
1. 重量级锁Monitor
也称为管程或监视器锁介绍重量级锁需要和操作系统对象Monitor关联因此会涉及到内核态和用户态的转换优点安全性高常用于金融系统等缺点会阻塞其他线程状态的切换也会导致效率低
2. 轻量级锁 介绍 轻量级锁应用在多线程交叉访问锁对象的情况即不存在两个线程同时竞争一个锁对象 轻量级锁不需要和Monitor关联而是通过一个叫 Lock Record 对象在虚拟机内部标识因此不涉及到状态切换 一旦发生竞争就升级为重量级锁 优点不用访问Monitor避免了内核态和用户态的切换提高程序响应速度常见于秒杀活动场景 轻量级锁是否存在自旋优化 目前偏向于是没有的而是在升级为重量级锁之后会使用自旋优化有时间可查源码分析 3. 偏向锁
依据很多时候一个锁对象常常是被同一个线程使用。如果每次锁重入都需要加锁解锁耗费性能特点 线程加锁时锁对象会记录当前线程的ID如果该线程再次访问对应的临界资源就无需再加锁只适应无并发情况一旦出现竞争就升级为重量级锁 在java中一个对象被创建时默认其为偏向锁以101结尾 轻量级锁
无竞争时、线程交叉访问临界资源时可使用轻量级锁语法仍然是 synchronized一开始都是轻量级锁如果发现竞争就自动升级为重量级锁Lock Record 对象仅在轻量级锁中使用
static final Object obj new Object();
public static void method1(){synchronized (obj){//同步块 Amethod2();}
}
public static void method2(){synchronized (obj){//同步块 B}
}上面代码的工作原理 创建锁记录对象Lock Record Object每个线程都有一个锁记录结构如果需要加锁就在当前栈帧中新建一个锁记录对象。该对象包含以下内容 锁记录地址和状态地址表示锁记录对象自身地址00表示初始状态为轻量级锁Object reference存储要锁对象即代码中的 obj 的引用地址 锁记录对象中的Object reference指向锁对象同时通过CAS操作一种原子操作尝试将自己的 lock record 地址 00 和 锁对象的 Mark Word 01 交换 此时锁对象Mark Word就成了状态00即表示处于轻量级锁状态同时还存储了锁记录对象的地址锁记录对象也成功存储了锁对象的Mark Word内容以便之后恢复交换成功即加锁成功其他线程访问锁对象发现其状态已经是轻量级锁状态00说明该锁已被其他对象使用CAS失败 如果CAS失败检查锁对象指向的地址是否在本线程的栈帧范围内 如果不是当前线程对其加锁那么表示有竞争将进入锁膨胀阶段如果是当前线程那就是 synchronized锁重入 如代码中的method2于是再添加一个Lock Record对象作为重入的计数 重入的Lock Record对象无需记录 Mark Word只需记录锁对象的地址 解锁 锁记录的值为null说明有重入删除null的锁记录对象即可锁记录的值不为nullCAS操作恢复自己的Mark Word 成功解锁成功失败说明轻量级锁进行了锁膨胀或已经升级为重量级锁进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中CAS操作无法成功说明该锁已被其他线程占用此时需要进行锁膨胀将轻量级锁变成重量级锁
场景Thread-0已经持有obj锁此时Thread-1也请求该锁
Thread-1希望获取锁对象 obj 执行CAS操作尝试将自己栈帧中的锁记录对象与锁对象obj的Mark Word进行交换时发现锁对象状态已经是 00 轻量级锁状态于是加锁失败进行锁膨胀锁膨胀流程 Thread-1为 锁对象obj 申请Monitor锁让 obj 指向重量级锁地址更改状态为 10Thread-1自身进入Monitor的EntryList BLOCKED 当Thread-0解锁时发现锁对象obj的指向地址已经不是自己解锁失败于是进入重量级解锁流程 根据锁对象obj里面的Monitor地址找到Monitor对象设置Owner为null唤醒EntryList中的BLOCKED线程
自旋优化
重量级锁竞争的时候可以通过自旋来进行优化即线程一直循环获取锁直到持锁线程释放锁从而避免线程阻塞
自旋成功 在自旋重试的过程中发现锁对象被释放于是成功加锁多核下才能实现 自旋失败 自旋多次之后进入阻塞状态
Java 6 之后自旋锁是自适应的如果对象的上次自旋成功那么就认为这次成功的可能性会高于是会多自旋几次反之少自旋甚至不自旋
自旋会占用CPU时间单核CPU自旋就是浪费多核才能发挥优势
Java 7 之后不能控制是否开启自旋功能
偏向锁
概念
在第一次加锁时通过CAS操作将线程ID设置到锁对象的Mark Word头里面 锁重入时不再新增锁记录对象而是比对锁记录对象中的线程ID如果是本线程就无需加锁直接使用 以后只要不发生竞争这个锁对象就归本线程所有 在一开始的时候JVM不知道使用的是偏向锁还是轻量级锁所以会在synchronized开始就创建一个Lock Record 确定为偏向锁后就不存在指向Lock Record的指针 偏向状态 默认开启 开启了偏向锁后那么对象创建后Mark Word 后三位即为101biased_lock1, status01thread, epochage都为0 查看Java对象的对象头初始状态 dependencygroupIdorg.openjdk.jol/groupIdartifactIdjol-core/artifactIdversion0.9/version
/dependencylog.debug(ClassLayout.parseInstance(obj).toPrintable());64位系统下Mark Word占 64 bits 从最后3位可以看到初始状态为001无偏向锁Normal状态 之所以是001而不是101是因为偏向锁默认是延迟的不会在程序启动时立即生效可以sleep(4000)来观察 如果希望避免延迟可以加VM参数来禁用延迟 -XX:BiasedLockingStartupDelay0测试偏向锁加锁之后 synchronized (obj){log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(ClassLayout.parseInstance(obj).toPrintable());这里的线程ID是操作系统设置的唯一标识和Java设置的Thread-1之类的标识不通 禁用偏向锁 -XX:-UseBiasedLocking撤销偏向锁 撤销偏向锁会使其升级为轻量级锁/重量级锁 偏向锁重偏向是更改偏向的线程 1. 调用hashCode
log.debug(ClassLayout.parseInstance(obj).toPrintable());
obj.hashCode();
synchronized (obj){log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(ClassLayout.parseInstance(obj).toPrintable());调用hashCode会禁用掉偏向锁直接使用重量级锁上述代码后3位执行时从101 -- 000 - 001
轻量级锁和重量级锁调用hashCode之后不会出现这个问题 轻量级锁的hashCode会存在Lork Record里面重量级锁会存在Monitor里面可以反复交换 偏向锁的Mark Word只能一个数据覆盖一个另一个
2. 其他线程使用对象
当有其他线程使用偏向锁对象时偏向锁会升级为轻量级锁后3位从 101 变为 000
即便不竞争两个线程以先后顺序访问锁对象都会导致偏向锁升级为轻量级锁如果存在竞争则进一步升级为重量级锁
3. 调用wait/notify
wait和notify只有重量级锁才有因此调用时需要先升级为重量级锁
批量重偏向和批量撤销
输出JVM的默认参数值
-XX:PrintFlagsFinal批量重偏向
重定向即更改偏向锁指向的Thread且并不会升级为其他锁
设置偏向锁批量重偏向阈值
-XX:BiasedLockingBulkRebiasThreshold 20上面的命令代表当撤销重定向的次数达到20次时jvm就认为偏向错误于是更改偏向的线程
举例https://blog.csdn.net/weixin_33255691/article/details/114770537
线程1初始时获取了50个锁对象于是这50个锁对象都是偏向锁线程1运行结束释放锁资源此时这50个锁对象都偏向线程1线程2需要用到线程1使用过的前30个锁对象根据撤销偏向锁里介绍的锁会升级为轻量级锁最终结果 前19个锁对象升级成为轻量级锁第20~30个锁对象更改偏向对象偏向线程2第31~40个锁对象未更改仍偏向线程1
批量撤销
当撤销偏向锁阈值达到40次之后jvm就认为根本不该偏向于是整个类的所有对象都变为不可偏向新建的锁对象也变为不可偏向
默认偏向锁批量撤销阈值
-XX:BiasedLockingBulkRevokeThreshold 40在同一次运行中一个对象最多重偏向1次第2次重偏向时会变为000轻量级锁
举例
线程1初始时获取了60个锁对象于是这60个锁对象都是偏向锁 第1-60个偏向1 线程2对这60个锁对象再次加锁 前1-19个变为轻量级锁第20-60个偏向2 线程3对第20-39个锁再次加锁 前1-19个已经是轻量级锁所以这里没有使用它们第20-39轻量级锁 之后创建的新锁000无锁状态不可加锁
Slf4j(topic c.test)
public class ConcurrentApplication {static Thread t1, t2, t3;public static void main(String[] args) throws InterruptedException {VectorDog locks new Vector();//线程1使得60个锁对象成为偏向锁t1 new Thread(()-{for(int i0; i60; i){Dog obj new Dog();locks.add(obj);synchronized (obj){if(i 18){//打印线程1关键节点的锁对象状态log.debug(线程1第 {} 个锁对象的对象头{}, i1, ClassLayout.parseInstance(locks.get(i)).toPrintable());}}}LockSupport.unpark(t2);}, t1);t1.start();//线程2撤销前60个锁对象t2 new Thread(()-{LockSupport.park();for(int i0; i60; i){Dog obj locks.get(i);synchronized (obj){if(i 18 || i19 || i38 || i39 || i58 || i59){//if(i 18 || i19){//打印线程2关键节点的锁对象状态log.debug(线程2第 {} 个锁对象的对象头{}, i1, ClassLayout.parseInstance(locks.get(i)).toPrintable());}}}LockSupport.unpark(t3);}, t2);t2.start();//线程3撤销前20~40个锁对象t3 new Thread(()-{LockSupport.park();for(int i20; i39; i){Dog obj locks.get(i);synchronized (obj){if(i 18 || i19 || i38 || i39 || i58 || i59){//if(i 18 || i19){//打印线程3关键节点的锁对象状态log.debug(线程3第 {} 个锁对象的对象头{}, i1, ClassLayout.parseInstance(locks.get(i)).toPrintable());}}}}, t3);t3.start();t3.join();log.debug(新的锁对象的对象头{}, ClassLayout.parseInstance(new Dog()).toPrintable());}
}只能重偏向一次2次重偏向的话会升级成轻量级锁并且释放锁之后变成不可偏向 疑惑 根据实验结果t1获取100个锁t2重偏向这100个锁最终新的对象也不会出现不可加锁状态 考虑这种结论所谓批量撤销阈值达到40是否是指二次偏向的阈值达到20 锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除这是 JVM 即时编译器的优化
锁消除主要是通过逃逸分析来支持如果堆上的共享数据不可能逃逸出去被其它线程访问到那么就可以把它们当成私有数据对待也就可以将它们的锁进行消除同步消除JVM 逃逸分析
默认打开设置关闭
-server -XX:DoEscapeAnalysis -XX:EliminateLocks然后在打开/关闭的状态下依次测试下列代码
public static String getString(String s1, String s2) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();
}public static void main(String[] args) {long tsStart System.currentTimeMillis();for (int i 0; i 1000000; i) {getString(TestLockEliminate , Suffix);}System.out.println(一共耗费 (System.currentTimeMillis() - tsStart) ms);
}append是一个synchronized代码但这里的 sb 是一个局部变量因此会被 JIT 即时编译器优化
3. 同步
3.1 wait notify
底层原理
Owner 线程发现条件不满足调用 wait 方法即可进入 WaitSet 变为 WAITING 状态BLOCKED 和 WAITING 的线程都处于阻塞状态不占用 CPU 时间片BLOCKED 线程会在 Owner 线程释放锁时唤醒WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒唤醒后并不意味者立刻获得锁需要进入 EntryList 重新竞争调用wait()之后会释放占用的锁资源
API
obj.wait() 让进入 object 监视器的线程到 waitSet 等待wait()时会释放锁obj.wait(n) 无参wait实际上是调用了wait(0)带参是有时限的等待obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
必须获得此对象的锁才能调用这几个方法
new Thread(() - {synchronized (obj) {try {obj.wait(); // 让线程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}}
}).start();sleep(long n) 和 wait(long n) 的区别
sleep 是 Thread 方法而 wait 是 Object 的方法sleep 不需要强制和 synchronized 配合使用但 wait 需要和 synchronized 一起用sleep 在睡眠的同时不会释放对象锁的但 wait 在等待的时候会释放对象锁共同点它们状态都将成为 TIMED_WAITING
wait()的使用方法
synchronized(lock){while(条件不成立){lock.wait()}
}
//另一个线程
synchronized(lock){lock.notifyAll();
}3.2 同步模式之保护性暂停 一对一模型 Guarded Suspension
即 Guarded Suspension用在一个线程等待另一个线程的执行结果
有一个结果需要从一个线程传递到另一个线程让他们关联同一个 GuardedObject如果有结果不断从一个线程到另一个线程那么可以使用消息队列见生产者/消费者JDK 中join 的实现、Future 的实现采用的就是此模式因为要等待另一方的结果因此归类到同步模式
public static void main(String[] args) {GuardObject guardObject new GuardObject();new Thread(() - {log.debug(等待结果);ListString list (ListString) guardObject.get();//list.stream().map(String::toUpperCase).forEach(log::debug);log.debug(Arrays.toString(list.toArray()));}, t1).start();new Thread(() - {log.debug(执行下载);sleep(2);//模拟下载时间ListString list new ArrayListString(){{add(one); add(two);}};guardObject.complete(list);}).start();
}class GaurdObject{private Object response;private final Object lock new Object();//获取结果response//通过while和wait不断询问结果准备好了没public Object get(){synchronized (lock){while(response null){try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}return response;}}public void complete(Object response){synchronized (lock){this.response response;lock.notifyAll();}}
}带时限的等待
public Object get(long timeout){synchronized (this){long begin System.currentTimeMillis();long passedTime 0;while(response null){long waitTime timeout - passedTime;if(waitTime 0) break;try {//this.wait(timeout);//假设timeout是2秒在这里虚假唤醒下一次循环时剩下wait时间应当是1秒而非2秒this.wait(waitTime);} catch (InterruptedException e) {e.printStackTrace();}passedTime System.currentTimeMillis() - begin;}return response;}
}join 原理
join实际上是通过wait实现的
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);//相当于wait()无限等待}} else {while (isAlive()) {long delay millis - now;if (delay 0) {break;}wait(delay);now System.currentTimeMillis() - base;}}
}多任务版 Guard Suspension 解耦 结果生产者 和 结果等待者
流程说明
每个居民开启一个线程申请一个GuardObject对象然后调用对象的get方法等待邮递员线程工作每个邮递员开启一个线程获取信箱里所有的GuardObject的id并根据id设置送信内容mail邮递员根据id获取居民申请的GuardObject对象然后将mail传进去并唤醒所有居民每个居民被唤醒后去检查自己的response是否为空从而完成收信
//一一对应的模式
package com.example;Slf4j(topic c.Test)
public class ConcurrentApplication{public static void main(String[] agrs){//各个居民只需开启收信功能for(int i0; i3; i){new People().start();//等待送信}TimeUnit.SECONDS.sleep(1);//邮递员依次检查信箱是否有信要送这里设置的是每个居民有一个信箱而有多少个信件就雇佣多少个邮递员for(Integer id : MailBoxes.getIds()){new Postman(id, messageid).start();}}
}//居民
Slf4j(topic c.People)
class People extends Thread{Overridepublic void run() {GuardObject guardObject MailBoxes.createGuardObject();Object mail guardObject.get(5000);log.debug(居民 {} 收到了信件 {}, guardObject.getId(), mail);}
}//邮递员
Slf4j(topic c.Postman)
class Postman extends Thread{private int id;private String mail;//Postman去信箱里获取送信地址id和送信内容mailpublic Postman(int id, String mail){this.id id; this.mail mail;}Overridepublic void run() {log.debug(邮递员发现了居民{}的信件内容为{}, id, mail);GuardObject guardObject MailBoxes.getGuardObject(id);guardObject.complete(mail);log.debug(已向居民{}送信内容为{}, guardObject.getId(), mail);}
}//解耦类信箱
class MailBoxes{private static MapInteger, GuardObject boxes new HashMap();private static int id;public static synchronized int generateId(){ return id;}public static GuardObject createGuardObject(){GuardObject guardObject new GuardObject(generateId());boxes.put(guardObject.getId(), guardObject);return guardObject;}public static GuardObject getGuardObject(int id){return boxes.remove(id);}public static SetInteger getIds(){return boxes.keySet();}
}class GuardObject{private int id;public GuardObject(int id){this.id id;}public int getId() {return id;}private Object response;public Object get(long timeout){synchronized (this){long begin System.currentTimeMillis();long passTime 0;while (response null){long waitTime timeout - passTime;if(waitTime 0) break;try {this.wait(waitTime);} catch (InterruptedException e) {e.printStackTrace();}passTime System.currentTimeMillis() - begin;}return response;}}public void complete(Object response){synchronized (this){this.response response;this.notifyAll();}}
}结果
15:49:58.820 [Thread-7] DEBUG c.Postman - 邮递员发现了居民2的信件内容为message2
15:49:58.820 [Thread-5] DEBUG c.Postman - 邮递员发现了居民0的信件内容为message0
15:49:58.820 [Thread-6] DEBUG c.Postman - 邮递员发现了居民1的信件内容为message1
15:49:58.823 [Thread-7] DEBUG c.Postman - 已向居民2送信内容为message2
15:49:58.823 [Thread-5] DEBUG c.Postman - 已向居民0送信内容为message0
15:49:58.823 [Thread-2] DEBUG c.People - 居民 0 收到了信件 message0
15:49:58.823 [Thread-3] DEBUG c.People - 居民 2 收到了信件 message2
15:49:58.823 [Thread-6] DEBUG c.Postman - 已向居民1送信内容为message1
15:49:58.823 [Thread-1] DEBUG c.People - 居民 1 收到了信件 message13.3 同步模式之生产者/消费者 n对n模型 Guarded Suspension是通过wait使自己处于阻塞状态来等待收信是典型的同步模式 注意在课程中说生产者/消费者是异步模型但鉴于wait仍需阻塞等待这里个人理解将其归于同步模型 与前面的保护性暂停中的 GuardObject 不同不需要产生结果和消费结果的线程一一对应消费队列可以用来平衡生产和消费的线程资源生产者仅负责产生结果数据不关心数据该如何处理而消费者专心处理结果数据消息队列是有容量限制的满时不会再加入数据空时不会再消耗数据JDK 中各种阻塞队列采用的就是这种模式生产者/消费者是在java线程间通信而非进程间通信 package com.example;Slf4j(topic c.Test)
public class ConcurrentApplication{public static void main(String[] agrs){//先创建一个消息队列MessageQueue queue new MessageQueue(2);//模拟3个生产者和1个消费者线程的情况for(int i0; i3; i){int finalI i;new Thread(()-{//匿名内部类引用的局部变量应当声明为finalqueue.put(new Message(finalI, message finalI));}, 生产者i).start();}new Thread(()-{while(true){TimeUnit.SECONDS.sleep(1);//每隔1秒取一次消息Message message queue.take();}}, 消费者).start();}
}//消息队列类java线程之间通信
Slf4j(topic c.MessageQueue)
class MessageQueue{private LinkedListMessage list new LinkedList();//创建一个双向队列作为消息的队列集合private int capacity;//消息队列容量public MessageQueue(int capacity){this.capacitycapacity;}//1. 获取消息public Message take(){//检查队列是否为空synchronized (list){while(list.isEmpty()){log.debug(队列为空请消费者线程等待);try {list.wait();} catch (InterruptedException e) {e.printStackTrace();}}//从队列头部获取消息Message message list.removeFirst();log.debug(取出消息 {} 此时容量为{}, message.getId(), list.size());list.notifyAll();return message;}}//2. 存入消息public void put(Message message){synchronized (list){//检查队列是否已满while(list.size() capacity){log.debug(队列已满请生产者线程等待);try {list.wait();} catch (InterruptedException e) {e.printStackTrace();}}list.addLast(message);log.debug(存入消息 {}此时容量为{}, message.getId(), list.size());list.notifyAll();}}
}
//消息结构
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;}
}结果
16:29:26.626 [生产者0] DEBUG c.MessageQueue - 存入消息 0此时容量为1
16:29:26.628 [生产者2] DEBUG c.MessageQueue - 存入消息 2此时容量为2
16:29:26.628 [生产者1] DEBUG c.MessageQueue - 队列已满请生产者线程等待
16:29:27.628 [消费者] DEBUG c.MessageQueue - 取出消息 0 此时容量为1
16:29:27.629 [生产者1] DEBUG c.MessageQueue - 存入消息 1此时容量为2
16:29:28.631 [消费者] DEBUG c.MessageQueue - 取出消息 2 此时容量为1
16:29:29.640 [消费者] DEBUG c.MessageQueue - 取出消息 1 此时容量为0
16:29:30.645 [消费者] DEBUG c.MessageQueue - 队列为空请消费者线程等待3.4 pack和unpack
基本使用
LockSupport.park();// 暂停当前线程
LockSupport.unpark(线程);// 恢复某个线程的运行特点
与 Object 的 wait notify 相比不同点
waitnotify 和 notifyAll 必须配合 Object Monitor 一起使用而 parkunpark 不必park unpark 是以线程为单位来【阻塞】和【唤醒】线程而 notify 只能随机唤醒一个等待线程notifyAll 是唤醒所有等待线程就不那么精确park unpark 可以先 unpark而 wait notify 不能先 notify
相同点
park()之后也会进入无时限Waiting状态
原理
每个线程都有自己的一个Parker对象由三部分组成
_counter标识0标识线程已被阻塞1表示未被阻塞
_cond阻塞队列
_mutex park() 当前线程调用 Unsafe.park() 方法设置_counter0检查 _counter 的前值 如果前值0获取 _mutex 互斥锁线程进入 _cond 条件变量阻塞如果前值1继续运行 unpark() 调用 Unsafe.unpark(Thread_0) 方法设置_counter1检查 _counter 的前值 如果前值0获取_mutex互斥锁将线程从_cond阻塞队列中将他唤醒如果前值1继续运行
3.5 线程状态
从操作系统层面来看有五种状态
初始状态仅是在语言层面创建了线程对象还未与操作系统线程关联可运行状态就绪状态指该线程已经被创建与操作系统线程关联可以由 CPU 调度执行运行状态指获取了 CPU 时间片运行中的状态阻塞状态如果调用了阻塞 API如 BIO 读写文件这时该线程实际不会用到 CPU终止状态表示线程已经执行完毕生命周期已经结束不会再转换为其它状态
线程中Java APIThread.state定义了六种状态 NEW线程刚被创建但是还没有调用 start() 方法 RUNNABLE当调用了 start() 方法之后 Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】由于 BIO 导致的线程阻塞在 Java 里无法区分仍然认为是可运行 BLOCKED【阻塞状态】的细分 如 等待未被释放的锁 WAITING 【阻塞状态】的细分 无时限的等待如 t2.join()但t2是一个死循环 TIMED_WAITING 【阻塞状态】的细分 有时限的等待如 sleep(10000) TERMINATED当线程代码运行结束
3.6 线程状态的转换 以 t 表示线程t 以 obj 表示synchronized之后获取的锁对象 new -- runnable t.start()runnable – waiting obj.wait() - obj.wait() runnable -- waiting
- obj.notify(), obj.notifyAll(), t.interrupt()竞争锁成功waiting -- runnable竞争锁失败waiting -- blockedt.join() - t.join()runnable -- waiting注意是当前线程在t线程对象的监视器上等待
- t.interrupt()打断joinwaiting -- runnablepark() 和 unpark() - LockSupport.park()runnable -- waiting
- LockSupport.unpark() 或 t.interrupt()waiting -- runnablerunnable – timed_waiting obj.wait(long n) - obj.wait(long n)runnable -- timed_waiting
- 时间超过nobj.notify(), obj.notifyAll(), t.interrupt()竞争锁成功waiting -- runnable竞争锁失败waiting -- blockedt.join(long n) - t.join(long n)runnable -- timed_waiting注意是当前线程在t线程对象的监视器上等待
- 时间超过nt线程结束interrupttimed_waiting -- runnableThread.sleep(long n) - Thread.sleep(long n)runnable -- timed_waiting
- 时间超过ntimed_waiting -- runnableparkNanos(long nanos) 和 parkUntil(long millis) - LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis)runnable -- timed_waiting
- LockSupport.unpark(目标线程)interrupt()等待超时timed_waiting -- runnablerunnable – blocked 竞争锁失败 - synchronized(obj)失败runnable -- blocked
- 锁被释放时会唤醒该对象上所有的BLOCKED线程如果竞争成功blocked -- runningrunnable -- terminated 当前线程的所有代码运行完毕后 文章转载自: http://www.morning.bpmnh.cn.gov.cn.bpmnh.cn http://www.morning.dyxzn.cn.gov.cn.dyxzn.cn http://www.morning.ywpcs.cn.gov.cn.ywpcs.cn http://www.morning.yxdrf.cn.gov.cn.yxdrf.cn http://www.morning.rshs.cn.gov.cn.rshs.cn http://www.morning.tdwjj.cn.gov.cn.tdwjj.cn http://www.morning.ktmnq.cn.gov.cn.ktmnq.cn http://www.morning.mlfmj.cn.gov.cn.mlfmj.cn http://www.morning.yrpg.cn.gov.cn.yrpg.cn http://www.morning.mgmyt.cn.gov.cn.mgmyt.cn http://www.morning.pqbkk.cn.gov.cn.pqbkk.cn http://www.morning.ygkq.cn.gov.cn.ygkq.cn http://www.morning.nqgjn.cn.gov.cn.nqgjn.cn http://www.morning.dzyxr.cn.gov.cn.dzyxr.cn http://www.morning.rqkck.cn.gov.cn.rqkck.cn http://www.morning.rczrq.cn.gov.cn.rczrq.cn http://www.morning.nktgj.cn.gov.cn.nktgj.cn http://www.morning.gdgylp.com.gov.cn.gdgylp.com http://www.morning.deupp.com.gov.cn.deupp.com http://www.morning.dnmzl.cn.gov.cn.dnmzl.cn http://www.morning.zpzys.cn.gov.cn.zpzys.cn http://www.morning.wbqk.cn.gov.cn.wbqk.cn http://www.morning.rdzlh.cn.gov.cn.rdzlh.cn http://www.morning.kzslk.cn.gov.cn.kzslk.cn http://www.morning.wmyqw.com.gov.cn.wmyqw.com http://www.morning.bwzzt.cn.gov.cn.bwzzt.cn http://www.morning.fsfz.cn.gov.cn.fsfz.cn http://www.morning.schwr.cn.gov.cn.schwr.cn http://www.morning.jmnfh.cn.gov.cn.jmnfh.cn http://www.morning.wfbs.cn.gov.cn.wfbs.cn http://www.morning.wjxyg.cn.gov.cn.wjxyg.cn http://www.morning.gqjzp.cn.gov.cn.gqjzp.cn http://www.morning.rgrz.cn.gov.cn.rgrz.cn http://www.morning.rdfq.cn.gov.cn.rdfq.cn http://www.morning.ohmyjiu.com.gov.cn.ohmyjiu.com http://www.morning.jtkfm.cn.gov.cn.jtkfm.cn http://www.morning.dmtwz.cn.gov.cn.dmtwz.cn http://www.morning.lxlfr.cn.gov.cn.lxlfr.cn http://www.morning.rhlhk.cn.gov.cn.rhlhk.cn http://www.morning.qfmcm.cn.gov.cn.qfmcm.cn http://www.morning.xrpjr.cn.gov.cn.xrpjr.cn http://www.morning.qygfb.cn.gov.cn.qygfb.cn http://www.morning.xqmd.cn.gov.cn.xqmd.cn http://www.morning.ssgqc.cn.gov.cn.ssgqc.cn http://www.morning.kfmnf.cn.gov.cn.kfmnf.cn http://www.morning.2d1bl5.cn.gov.cn.2d1bl5.cn http://www.morning.jwrcz.cn.gov.cn.jwrcz.cn http://www.morning.rpfpx.cn.gov.cn.rpfpx.cn http://www.morning.tllhz.cn.gov.cn.tllhz.cn http://www.morning.lqqqh.cn.gov.cn.lqqqh.cn http://www.morning.jjhrj.cn.gov.cn.jjhrj.cn http://www.morning.stmkm.cn.gov.cn.stmkm.cn http://www.morning.cbnxq.cn.gov.cn.cbnxq.cn http://www.morning.wbrf.cn.gov.cn.wbrf.cn http://www.morning.rbhqz.cn.gov.cn.rbhqz.cn http://www.morning.phxns.cn.gov.cn.phxns.cn http://www.morning.tcpnp.cn.gov.cn.tcpnp.cn http://www.morning.bnfjh.cn.gov.cn.bnfjh.cn http://www.morning.pzcjq.cn.gov.cn.pzcjq.cn http://www.morning.21r000.cn.gov.cn.21r000.cn http://www.morning.hytfz.cn.gov.cn.hytfz.cn http://www.morning.lkkkf.cn.gov.cn.lkkkf.cn http://www.morning.fjzlh.cn.gov.cn.fjzlh.cn http://www.morning.ltrz.cn.gov.cn.ltrz.cn http://www.morning.xtdtt.cn.gov.cn.xtdtt.cn http://www.morning.dlhxj.cn.gov.cn.dlhxj.cn http://www.morning.qykxj.cn.gov.cn.qykxj.cn http://www.morning.pbtdr.cn.gov.cn.pbtdr.cn http://www.morning.lnsnyc.com.gov.cn.lnsnyc.com http://www.morning.bytgy.com.gov.cn.bytgy.com http://www.morning.nyqm.cn.gov.cn.nyqm.cn http://www.morning.nmfwm.cn.gov.cn.nmfwm.cn http://www.morning.drmbh.cn.gov.cn.drmbh.cn http://www.morning.gdljq.cn.gov.cn.gdljq.cn http://www.morning.mjglk.cn.gov.cn.mjglk.cn http://www.morning.tymwx.cn.gov.cn.tymwx.cn http://www.morning.qbpqw.cn.gov.cn.qbpqw.cn http://www.morning.pzlhq.cn.gov.cn.pzlhq.cn http://www.morning.sfgtp.cn.gov.cn.sfgtp.cn http://www.morning.bpmdz.cn.gov.cn.bpmdz.cn