java之多线程三
1.线程不安全因素
1.抢占式执行
2.每个线程操作自己的变量
3.非原子操作
4.内存可见性
5.指令重排序
2.volatile 解决内存可见性和指令重排序
1.volatile可以解决内存可见性和指令重排序的问题,代码在写入volatile修饰变量的时候:
(1)改变线程工作内存中volatile变量副本的值。
(2)将改变后的副本的值从工作内存刷新到主内存。
2.代码在读取volatile1修饰的变量的时候:
(1)从主内存中读取volatile变量的最新值到线程的工作内存中。
(2)从工作内存中读取volatile变量的副本。
学习内存可见性时,我们知道是直接访问工作内存(实际是CPU的寄存器或者CPU的缓存),速度非常快,但是可能出现数据不一致的情况。
加上volatile,强制读写内存,速度是慢,但是数据变的更准确了。
3.内存可见性问题:
import java.time.LocalDateTime;
/**
* 内存可见性问题
*/
public class ThreadDemo17 {
//全局变量(类级别)
private static boolean flag=true;
public static void main(String[] args) {
//1.创建子线程1
System.out.println("线程1开始执行"+ LocalDateTime.now());
Thread t1=new Thread(()->{
while(flag){
}
System.out.println("线程1结束执行!");
});
t1.start();
Thread t2=new Thread(()->{
//休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2修改 flag=false"+LocalDateTime.now());
flag=false;
});
t2.start();
}
}
线程1一直在执行,它并没有感知到全局变量flag的变化,这就是内存可见性问题(因为线程2已经把全局变量修改为另一个值了)。
4.volatile可以解决内存可见性问题和指令重排序问题:
3.volatile缺点
1.volatile虽然可以解决内存可见性和指令重排序问题,但是解决不了原子性问题,因此对于 ++ 和 – 操作的线程⾮安全问题依然解决不了,⽐如以下代码:
public class ThreadDemoVolatile {
static class Counter{
//变量
private volatile int number=0;
//循环次数
private int MAX_COUNT=0;
public Counter(int MAX_COUNT) {
this.MAX_COUNT = MAX_COUNT;
}
//++方法
public void incr(){
for (int i = 0; i <MAX_COUNT ; i++) {
number++;
}
}
//--方法
public void decr(){
for (int i = 0; i < MAX_COUNT; i++) {
number--;
}
}
public int getNumber(){
return number;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter(100000);
Thread t1=new Thread(()->{
counter.incr();
});
t1.start();
Thread t2=new Thread(()->{
counter.decr();
});
t2.start();
//等待线程执行完成
t1.join();
t2.join();
System.out.println("最终的结果是:"+counter.getNumber());
}
}
注意事项:volatile并不能解决原子性问题,它只能解决内存可见性问题和指令重排序问题。
推断结论:使用volatile并不能完全解决线程安全问题。
4.锁
1.使用锁是java中解决线程安全最主要的手段。
2.java中的锁最主要的有以下两种:
(1)内置锁:synchronized
(2)可重入锁:Lock(ReentrantLock)
4.1synchronized
synchronized的基本用法有以下3种:
4.1.1修饰静态方法
/**
* synchronized修饰静态方法
*/
public class ThreadSynchronized {
private static int number=0;
static class Counter{
//循环次数
private static int MAX_COUNT=1000000;
//++方法
public synchronized static void incr(){
for (int i = 0; i < MAX_COUNT; i++) {
number++;
}
}
//--方法
public synchronized static void decr(){
for (int i = 0; i <MAX_COUNT ; i++) {
number--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
Counter .incr();
});
t1.start();
Thread t2=new Thread(()->{
Counter.decr();
});
t2.start();
//等待线程执行完成
t1.join();
t2.join();
System.out.println("最终的结果是:"+number);
}
}
4.1.2修饰普通方法
/**
* synchronized修饰普通方法
*/
public class ThreadSynchronized2 {
private static int number=0;
static class Counter{
//循环次数
private static int MAX_COUNT=1000000;
//++方法
public synchronized void incr(){
for (int i = 0; i <MAX_COUNT ; i++) {
number++;
}
}
//--方法
public synchronized void decr(){
for (int i = 0; i <MAX_COUNT ; i++) {
number--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
Thread t1=new Thread(()->{
counter.incr();
});
t1.start();
Thread t2=new Thread(()->{
counter.decr();
});
t2.start();
//等待线程执行完成
t1.join();
t2.join();
System.out.println("最终的结果:"+number);
}
}
4.1.3修饰代码块
1.在普通方法中,this表示当前对象的实例,有new时可以使用:
注意事项:使用synchronized时,一定要注意,对于同一个业务的多个线程加锁对象,一定要是同一个对象(加同一把锁)。
2.在静态方法中使用类对象加锁
注意事项:syncharonized修饰代码块,代码块在静态方法中时,不能使用this对象。
3.自定义锁对象
/**
* synchronized修饰代码块
*/
public class ThreadSynchronized3 {
private static int number;
static class Counter{
//循环次数
private static int MAX_COUNT=1000000;
//自定义锁对象(属性名可以自定义)
private Object mylock=new Object();
//++方法
public void incr(){
for (int i = 0; i <MAX_COUNT ; i++) {
synchronized (mylock){
number++;
}
}
}
//--方法
public void decr(){
for (int i = 0; i <MAX_COUNT ; i++) {
synchronized (mylock){
number--;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
Thread t1=new Thread(()->{
counter.incr();
});
t1.start();
Thread t2=new Thread(()->{
counter.decr();
});
t2.start();
//等待线程执行完成
t1.join();
t2.join();
System.out.println("最终的结果:"+number);
}
}
4.1.4总结
synchronized用法有3种:
1.修饰静态方法
2.修饰普通方法
3.修饰代码块
synchronized(对象) { //........ }
(1)this(非静态方法)
(2)xxx.class(静态方法)
(3)自定义类对象(使用最多)
4.2synchronized特性
4.2.1互斥性(排他性)
1.synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其它线程如果也执行到同一个对象synchronized就会阻塞等待。
(1)进入synchronized修饰的代码块,相当于加锁。
(2)退出synchronized修饰的代码块,相当于解锁。
synchronized用的锁是存在java对象头里的:
可以粗略理解成,每个对象在内存中存储的时候,都存有一块内存表示当前的“锁定”状态。如果当前未被锁定,那么可以使用,使用是设置锁状态为占用,如果当前是占用状态,那么其他人无法使用,只能排队等候。
(3)阻塞等待
针对每一把锁,操作系统内部维护了一个等待队列,当这个锁被某个线程占用的时候,其他线程尝试进行加锁,就加不上,就会阻塞等待,已知等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁。
注意:
a.上一线程解锁之后,下一个线程并不是立即就能获取到锁,而是靠操作系统来“唤醒”,这也是操作系统线程调度的一部分工作。
b.假设有A,B,C三个线程,线程A先获取到锁,然后B尝试获取锁,然后C再尝试获取锁,此时B和C都在阻塞队列中排队等待,但是当A释放锁之后,虽然B比C先来的,但是B不一定就能获取到锁,而是和C重新竞争,并不遵守先来后到的规则。
4.2.2刷新内存
1.synchonized的工作过程:
(1)获得互斥锁
(2)从主内存拷贝变量的最新副本到工作的内存。
(3)执行代码
(4)将更改后的共享变量的值刷新到主内存
(5)释放互斥锁。
所以synchronized也能保证内存可见性。
4.2.3可重入性
syncharonized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
/**
* synchronized可重入性测试
*/
public class ThreadSynchronized4 {
public static void main(String[] args) {
synchronized (ThreadSynchronized4.class){
System.out.println("当前主线程已经得到了锁");
synchronized (ThreadSynchronized4.class){
System.out.println("当前主线程再次得到了锁");
}
}
}
}
4.2.4注意事项
1.加同一把锁
2.实例类可以使用this,静态类使用xxx.class.
4.3synchronized底层实现和运行原理
synchronized同步锁是通过JVM内置的Monitor监视器实现的,而监视器又是依赖操作系统的互斥锁Mutex实现的。
4.3.1监视器
1.监视器是一个概念或者说是一个机制,它用来保障在任何时候,只有一个线程能够执行指定区域的代码。
⼀个监视器像是⼀个建筑,建筑⾥有⼀个特殊的房间,这个房间同⼀时刻只能被⼀个线程所占有。⼀ 个线程从进⼊该房间到离开该房间,可以全程独占该房间的所有数据。进⼊该建筑叫做进⼊监视器 (entering the monitor),进⼊该房间叫做获得监视器(acquiring the monitor),独⾃占有该房间 叫做拥有监视器(owning the monitor),离开该房间叫做释放监视器(releasing the monitor), 离开该建筑叫做退出监视器(exiting the monitor)。
4.3.2底层实现
实现一个synchronized代码块,来观察一下它在字节码层面是如何实现的?
将上面编译成字节码之后,得到的结果如下:
上述结果我们发现,在main方法中多了monitorenter和monitorexit的指令:
(1)monitorenter:表示进入监视器
(2)monitorexit:表示退出监视器
由此可知synchronized是依赖Monitor监视器实现的。
4.3.3执行流程
1.在java中,synchronized是非公平锁,也是可以重入锁。
(1)非公平锁:是指线程获取锁的顺序不是按照访问的顺序先来先到的,而是由线程自己竞争,随机获取锁。
(2)可重入锁:指的是一个线程获取到锁之后,可以重复得到该锁.
在 HotSpot 虚拟机中,Monitor 底层是由 C++实现的,它的实现对象是ObjectMonitor, ObjectMonitor 结构体的实现如下:
在上面代码中有几个关键属性:
(1)_count:记录该线程获取锁的次数(也就是前前后后,这个线程一共获取此锁多少次)
(2)_recursions:锁的重入次数。
(3)_owner:The Ower拥有者,是持有该ObjectMonitor(监视器)对象的线程。
(4)_EntryList:EntryList监控集合,存放的是处于阻塞状态的线程队列,当线程执行了wait()方法之后,会进入EntryList队列。
(5)_WaitSet:WaitSet待授权集合,存放的是处于 wait状态的线程队列,当线程执行了wait()方法之后,会进入WaitSet队列。
2.监视器执行流程如下:
1.线程通过 CAS(对⽐并替换)尝试获取锁,如果获取成功,就将 _owner 字段设置为当前线程,说 明当前线程已经持有锁,并将 _recursions 重⼊次数的属性 +1。如果获取失败则先通过⾃旋 CAS 尝试获取锁,如果还是失败则将当前线程放⼊到 EntryList 监控队列(阻塞)。
2.当拥有锁的线程执⾏了 wait ⽅法之后,线程释放锁,将 owner 变量恢复为 null 状态,同时将该线 程放⼊ WaitSet 待授权队列中等待被唤醒.
3. 当调⽤ notify ⽅法时,随机唤醒 WaitSet 队列中的某⼀个线程,当调⽤ notifyAll 时唤醒所有的 WaitSet 中的线程尝试获取锁.
4.线程执⾏完释放了锁之后,会唤醒EntryList 中的所有线程尝试获取锁。
执行流程图如下:
3.synchronized在jdk1.6之前使用的比较少,jdk1.6之前synchronized默认是使用重量级锁实现,所以性能比较差。syncharonized1.6优化:
无锁->偏向锁->轻量锁->重量级锁
4.3.4总结
1.synchronized同步锁是通过JVM内置的Monitor监视器实现的,而监视器又是依赖操作系统互斥锁Mutex实现。JVM监视器的执行流程是:线程先通自旋CAS的方式尝试获取锁,如果获取失败进入EntrySet集合,如果获取成功就拥有该锁。当调用wait()方法时,线程释放锁并进入WaitSet集合,等其他线程调用notify和notifyAll方法时再尝试获取锁。锁使用完释放之后就会通知EntrySet集合中的线程,让他们尝试获取锁。
2.synchronized是如何实现的?(面试必问)
答:JVM层面synchronized是依靠监视器Monitor实现的。
从操作系统层面来看,synchronized是基于操作系统的互斥锁(Mutex)来实现。
4.4Lock
4.4.1Lock基本用法
1.Lock实现步骤:
(1)创建Lock
(2)加锁lock.lock()
(3)释放锁lock.unlock()
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可重入锁基本使用
*/
public class ThreadLock {
public static void main(String[] args) {
//1.创建对象
Lock lock=new ReentrantLock();
//2.加锁操作
lock.lock();
System.out.println("你好 ReentrantLock");
//3.释放锁
lock.unlock();
}
}
改进后:
2.利用Lock方法解决线程安全问题:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock2 {
private static int number=0;
//1.创建锁对象
private static Lock lock=new ReentrantLock();
static class Counter{
//循环次数
private static int MAX_COUNT=1000000;
//++方法
public static void incr(){
for (int i = 0; i < MAX_COUNT; i++) {
//2.加锁操作
lock.lock();
try{
//业务代码
number++;
}finally{
//3.释放锁
lock.unlock();
}
}
}
//--方法
public static void decr(){
for (int i = 0; i <MAX_COUNT ; i++) {
//2.加锁操作
lock.lock();
try{
//业务代码
number--;
}finally{
//3.释放锁
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
Counter.incr();
});
t1.start();
Thread t2=new Thread(()->{
Counter.decr();
});
t2.start();
//等待线程执行完成
t1.join();
t2.join();
System.out.println("最终的结果是:"+number);
}
}
4.4.2 公平锁和非公平锁
1.Lock可以指定锁的类型,默认情况是创建一个非公平锁(性能高),传递参数true会创建一个公平锁。
2.非公平锁的执行时间:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock2 {
private static int number=0;
//1.创建锁对象
private static Lock lock=new ReentrantLock();
static class Counter{
//循环次数
private static int MAX_COUNT=1000000;
//++方法
public static void incr(){
for (int i = 0; i < MAX_COUNT; i++) {
//2.加锁操作
lock.lock();
try{
//业务代码
number++;
}finally{
//3.释放锁
lock.unlock();
}
}
}
//--方法
public static void decr(){
for (int i = 0; i <MAX_COUNT ; i++) {
//2.加锁操作
lock.lock();
try{
//业务代码
number--;
}finally{
//3.释放锁
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
long stime=System.currentTimeMillis();
Thread t1=new Thread(()->{
Counter.incr();
});
t1.start();
Thread t2=new Thread(()->{
Counter.decr();
});
t2.start();
//等待线程执行完成
t1.join();
t2.join();
long etime=System.currentTimeMillis();
System.out.println("最终的结果是:"+number+"|执行时间:"+(etime-stime));
}
}
3.公平锁的执行时间:
非公平锁执行效率高。
4.4.3Lock注意事项-放到try外
1.unlock操作一定要放在finally里面,因为如果不放在finally中可能会导致锁资源永久占用问题。
2.lock()一定要放在try之前或者是try的首行。
如果lock没有放在try之前,会产生两种问题:
(1)未加锁却执行了释放锁操作
当lock操作放在try方法内时:
lock操作如果放在 try 里面,因为 try 代码中的异常导致加锁失败,还会执⾏ finally 释放锁的操作,未加锁而释放锁会产生错误。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock3 {
public static void main(String[] args) {
Lock lock=new ReentrantLock();
try{
System.out.println("进入了try方法");
//某一种极端操作
int i=10/0;
System.out.println("执行lock操作");
lock.lock();
}finally{
System.out.println("执行unlock操作");
lock.unlock();
}
}
}
(2)释放锁的错误信息会覆盖业务报错信息,从而增加了调试程序和修复程序的复杂度。
4.5synchronized 和 Lock的区别(重点)
1.Lock更灵活,有更多的方法,比如tryLock().
2.锁类型不同:Lock默认是非公平锁,但可以指定为公平锁,synchronized只能为非公平锁。
3.调用lock方法和synchronized线程等待锁状态不同,lock方法会变为WAITING,synchronized会变成BLOCKED.
(1)调用lock方法:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock4 {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
System.out.println("执行 lock 操作");
Thread t1 = new Thread(() -> {
lock.lock();
System.out.println("线程1得到了锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程1释放锁");
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
System.out.println("线程2获取到了锁");
} finally {
lock.unlock();
}
});
t2.start();
Thread.sleep(1500);
System.out.println("线程2:" + t2.getState());
}
}
(2)synchronized
public class ThreadLock4 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (ThreadLock4.class) {
System.out.println("线程1得到了锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2释放锁");
}
});
t1.start();
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ThreadLock4.class) {
System.out.println("线程2获取到了锁");
}
});
t2.start();
Thread.sleep(1500);
System.out.println("线程2:" + t2.getState());
}
}
4.synchronized是JVM层面提供的锁,它是自定进行加锁和释放锁的操作,对于开发者是无感的,而Lock需要开发者自己进行加锁和释放锁的操作。
5.syncharonized可以修饰方法(静态方法/普通方法)和代码块,而Lock只能修饰代码。