Java内存模型与多线程:
线程不安全与线程安全:
线程安全问题阐述:
多条语句操作多个线程共享的资源时,一个线程只执行了部分语句,还没执行完,另一个线程又进来操作共享数据(执行语句),导致共享数据最终结果出现误差;所以就是看一个线程能否每次在没有其他线程进入的情况下操作完包含共享资源的语句块,如果能就没有安全问题,不能就有安全问题;
如何模拟多线程的安全问题:
用Thread.sleep()方法模拟; 放在哪:放在多线程操作共享数据的语句块之间(使正在运行的线程休息一会,让其他线程执行,就会出现共享数据错误的问题);
如何解决线程安全问题:
解决思想:
只有当一个线程执行完所有语句之后,才能让另外一个线程进来再进行操作;
具体操作:
加锁,对操作共享数据的代码块加锁,实现在一个线程操作共享数据时,其它线程不能再进来操作,直到本线程执行完之后其它线程才能进来执行;
哪些代码块需要加锁(同步):
明确每个线程都会操作的代码块;
明确共享资源;
明确代码块中操作共享资源语句块,这些语句块就是需要加锁的代码块;
具体解决方式:(以synchronized为例)
同步代码块:
synchronized(对象)
{
需要被同步的代码块;
}
同步方法:
就是把需要同步的代码块放到一个函数里面,代码块原来所在的函数里面可能还有其他不需要同步的代码块(所以不能每次直接同步原来所在的方法),需要仔细分析;
确保没有线程安全问题的两个前提:
至少有两个及两个以上的线程操作共享资源;
所有线程使用的锁是同一个锁;
注意:加了锁之后还出现线程安全问题的话,说明上面两个前提肯定没有全部满足;
想实现线程安全大致有三种方法:
多实例,也就是不使用单例模式了(单例模式在多线程下是不安全的);
使用java.util.concurrent下面的类库;
使用锁机制synchronized、lock方式;
为什么单例在多线程下是不安全的:
因为在多线程下可能会创建多个实例,不能保证原子性,违背设计单例模式的初衷;
synchronized:
详解:
隐式锁,同步锁,内置锁,监视器锁,可重入锁;
为了解决线程同步问题而生;
当用它来修饰一个代码块或一个方法时,能够保证在同一时刻最多只有一个线程执行该段代码(或方法);
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁(对象,锁必须是对象,就是引用类型,不能是基本数据类型)叫做互斥锁;每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池(谁进入锁池);对于任何一个对象,系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作;每个对象的锁只能分配给一个线程,因此叫做互斥锁;
是可重入锁:一个线程可以多次获得同一个对象的互斥锁;
使用同步机制获取互斥锁的规则说明:
如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝;(不解)
类的每个实例都有自己的对象级别锁(一个实例对象就是一个互斥锁,同一个类的两个实例对象对应的互斥锁是不一样的);当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁(就是当前对象的意思);
持有一个对象级别锁不会阻止该线程被交换出来(不解),也不会阻塞其他线程访问同一实例对象中的非synchronized代码;
持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外;
使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁;
类级别锁被特定类的所有实例共享,它用于控制对static成员变量以及static方法的并发访问;具体用法与对象级别锁相似;
synchronized的不同写法对于性能和执行效率的优劣程度排序:
同步方法体 < 同步方法块(锁不是最小的锁) < 同步方法块(锁是最小的锁);
显式锁:
详解:
为什么叫显式锁,因为所有加锁和解锁的方法都是显示的,即必须手动加锁和释放锁;
为了保证锁最终一定会被释放(可能会有异常发生),要把互斥区放在try语句块内,并在finally语句块中释放锁;尤其当有return语句时,return语句必须放在try字句中,以确保unlock()不会过早发生,从而将数据暴露给第二个任务;
采用lock加锁和释放锁的一般形式如下:
接口:Lock ReadWriteLock
实现类:ReentrantLock ReentrantReadWriteLock
ReentrantLock是Lock接口的实现类, ReentrantReadWriteLock是ReadWriteLock接口的实现类;
ReadWriteLock并不是Lock的子接口,只是ReadWriteLock借助Lock来实现读写两个锁的并存、互斥的机制;
Lock:是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显示的;
ReentrantLock:在竞争条件下,ReentrantLock的实现要比现在的synchronized实现更具有可伸缩性;(有可能在JVM的将来版本中改进synchronized的竞争性能)这意味着当许多线程都竞争相同锁定时,使用ReentrantLock的吞吐量通常要比synchronized好;换句话说,当许多线程试图访问ReentrantLock保护的共享资源时,JVM将花费较少的时间来调度线程,而用更多时间执行线程;ReentrantLock是在工作中对方法块加锁使用频率最高的;但ReentrantLock也有一个主要缺点:它可能忘记释放锁定;
Lock和synchronized的比较:
Lock使用起来比较灵活,但是必须有释放锁的动作配合;
Lock必须手动释放和开启锁,而synchronized不需要手动释放和开启锁;
Lock只适用于代码块锁,而synchronized对象之间是互斥关系;
ReadWriteLock接口:
提供了readLock和writeLock两种锁的操作机制;
一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程;也就是说读写锁适用的场合是一个共享资源被大量读取操作,而只有少量的写操作;
在ReadWriteLock中,每次读取共享数据就需要读取锁,当需要修改共享数据时就需要写入锁;看起来好像是两个锁,其实不是;
ReentrantReadWriteLock类:
ReadWriteLock接口唯一的实现类;
主要应用场景是:当有很多线程都从某个数据结构读取数据,而很少有线程对其进行修改时,在这种情况下,允许读取器线程共享访问是合适的,写入器线程依然必须是互斥访问的;
主要特性:
公平性:
重入性:
锁降级:
锁升级:
锁获取中断:
条件变量:
重入数:
以上概括起来就是读写锁的机制:读-读不互斥、读-写互斥、写-写互斥;
ReentrantReadWriteLock与ReentrantLock的比较:
相同点:都是显式锁,需要手动加锁解锁,都很适合高并发场景;
不同点:
ReentrantReadWriteLock是对ReentrantLock的复杂扩展,能适合更加复杂的业务;
ReentrantReadWriteLock能实现一个方法中读写分离的锁的机制,而ReentrantLock加锁解锁只有一种机制;
显式锁(Lock)和隐式锁(synchronized)的比较:
ReentrantLock主要增了如下的高级功能:
1、等待可中断:
当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,它对处理执行时间上的同步块很有帮助;而在等待由synchronized产生的互斥锁时,会一直阻塞,是不能被中断的;
2、可实现公平锁:
多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待;而非公平锁则不保证这点,在锁释放时,任何一个等待锁的线程都有机会获得锁; synchronized中的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(ture)来要求使用公平锁;
3、锁可以绑定多个条件:
ReentrantLock对象可以同时绑定多个Condition对象(名曰:条件变量或条件队列);而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含条件,但如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁;而ReentrantLock则无需这么做,只需要多次调用newCondition()方法即可;而且我们还可以通过绑定Condition对象来判断当前线程通知的是哪些线程(即与Condition对象绑定在一起的其他线程);
其他方面的比较:
synchronized:读写互斥、写写互斥、读读互斥(读读操作不会引发安全问题);ReentrantReadWriteLock(读写锁):读写互斥、写写互斥、读写不互斥;
悲观锁 与 乐观锁:
悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁;传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁;
乐观锁:
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制;乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁;
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量;但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适;
显示锁StampedLock:
该类是一个读写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写;
读不阻塞写的实现思路:
在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!
因为在读线程非常多而写线程比较少的情况下,写线程可能发生饥饿现象,也就是因为大量的读线程存在并且读线程都阻塞写线程,因此写线程可能几乎很少被调度成功!当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。所以读写都存在的情况下,使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的!
死锁:
在两段不同的逻辑都在等待对方的锁释放才能继续往下工作时,这个时候就会产生死锁;
1 /** 2 * 死锁 3 */ 4 package thread02; 5 6 class Count11 7 { 8 private byte[] lock1 = new byte[1]; 9 private byte[] lock2 = new byte[2]; 10 11 private int num = 0; 12 13 public void add1() 14 { 15 synchronized (lock1) 16 { 17 try 18 { 19 Thread.sleep(1001); // 模拟干活 20 } 21 catch (InterruptedException e) 22 { 23 e.printStackTrace(); 24 } 25 26 synchronized (lock2) // 产生死锁,一直在等待lock2对象释放锁 27 { 28 num += 1; 29 } 30 31 System.out.println(Thread.currentThread().getName() + " - " + num); 32 } 33 } 34 35 public void add2() 36 { 37 synchronized (lock2) 38 { 39 try 40 { 41 Thread.sleep(1001); 42 } 43 catch (InterruptedException e) 44 { 45 e.printStackTrace(); 46 } 47 48 synchronized (lock1) // 产生死锁,一直等待lock1对象释放锁 49 { 50 num += 1; 51 } 52 53 System.out.println(Thread.currentThread().getName() + " - " + num); 54 } 55 } 56 } 57 58 public class DeadLockTest01 59 { 60 public static void main(String[] args) 61 { 62 Count11 count = new Count11(); 63 64 ThreadA threadA = new ThreadA(count); 65 Thread t1 = new Thread(threadA); 66 t1.setName("线程A"); 67 68 ThreadB threadB = new ThreadB(count); 69 Thread t2 = new Thread(threadB); 70 t2.setName("线程B"); 71 } 72 73 } 74 75 class ThreadA implements Runnable 76 { 77 private Count11 count; 78 79 public ThreadA(Count11 count) 80 { 81 this.count = count; 82 } 83 84 @Override 85 public void run() 86 { 87 count.add1(); 88 } 89 } 90 91 class ThreadB implements Runnable 92 { 93 private Count11 count; 94 95 public ThreadB(Count11 count) 96 { 97 this.count = count; 98 } 99 100 @Override 101 public void run() 102 { 103 count.add2(); 104 } 105 }
死锁
volatile:
表面意思:易变的,不稳定的;
作用:修饰变量;告诉编译器,凡是被该关键字声明的变量都是易变的,不稳定的;所以不要试图对该变量使用缓存等优化机制,而应当每次都从它的内存地址中去读取值;
特性:
使用volatile标记的变量在读取或写入时不需要使用锁,这将减少产生死锁的概率,使代码保持整洁;
每次读取volatile的变量都要从它的内存地址中读取,但并不是每次修改完volatile的变量后都要立刻将它的值写回内存;也就是说volatile只提供内存可见性,而没有提供原子性;所以使用这个关键字做高并发的安全机制是不可靠的;
适用场景:
最好是那种只有一个线程修改变量,多个线程读取该变量的地方;也就是对内存可见性要求高,而对原子性要求低的地方;
volatile与加锁机制的主要区别:
加锁机制既可以保证可见性又可以确保原子性;而volatile变量只能保证可见性;
atomic:
详述:
atomic是不会阻塞线程(或者说只是在硬件级别上阻塞了),线程安全的加强版的volatile原子操作;
java.util.concurrent.atomic包里,多了一批原子操作,主要用于高并发环境下的高效程序处理;
1 /** 2 * 原子操作 3 */ 4 package thread02; 5 6 import java.util.concurrent.atomic.AtomicInteger; 7 8 public class AtomicIntegerTest01 9 { 10 public static void main(String[] args) 11 { 12 AtomicInteger ai = new AtomicInteger(0); 13 14 // 获取当前的值 15 System.out.println(ai.get()); 16 System.out.println("--------------"); 17 18 // 取当前的值,并设置新的值 19 System.out.println(ai.getAndSet(5)); 20 System.out.println(ai.get()); // 设置新值之后的当前值 21 System.out.println("--------------"); 22 23 // 获取当前的值,并自增 24 System.out.println(ai.getAndIncrement()); 25 System.out.println(ai.get()); // 自增之后的当前值 26 System.out.println("--------------"); 27 28 // 获取当前的值,并自减 29 System.out.println(ai.getAndDecrement()); 30 System.out.println(ai.get()); // 自减之后的当前值 31 System.out.println("--------------"); 32 33 // 获取当前的值,并加上预期的值 34 System.out.println(ai.getAndAdd(3)); 35 System.out.println(ai.get()); // 加上预期值之后的当前值 36 System.out.println("--------------"); 37 38 } 39 }
原子操作
单例:
1 /** 2 * 单例模式第一种写法:线程不安全的,不正确的写法 3 */ 4 package thread02.singleton; 5 6 public class Singleton01 7 { 8 private static Singleton01 instance; 9 10 private Singleton01() 11 { 12 13 } 14 15 public static Singleton01 getSingleton() 16 { 17 if(instance == null) 18 { 19 instance = new Singleton01(); 20 } 21 22 return instance; 23 } 24 }
单例模式第一种写法:线程不安全的,不正确的写法
1 /** 2 * 单例模式第二种写法:线程安全,但是高并发性能不是很高 3 */ 4 package thread02.singleton; 5 6 public class Singleton02 7 { 8 private static Singleton02 instance; 9 10 private Singleton02() 11 { 12 13 } 14 15 public static synchronized Singleton02 getSingleton() 16 { 17 if(instance == null) 18 { 19 instance = new Singleton02(); 20 } 21 22 return instance; 23 } 24 }
单例模式第二种写法:线程安全,但是高并发性能不是很高
1 /** 2 * 单例模式第三种写法:线程安全,性能又高,这种写法最为常用 3 */ 4 package thread02.singleton; 5 6 public class Singleton03 7 { 8 private static Singleton03 instance; 9 private static byte[] lock = new byte[0]; 10 11 private Singleton03() 12 { 13 14 } 15 16 public static Singleton03 getSingleton() 17 { 18 if(instance == null) 19 { 20 synchronized (lock) 21 { 22 if(instance == null) 23 { 24 instance = new Singleton03(); 25 } 26 } 27 } 28 29 return instance; 30 } 31 }
单例模式第三种写法:线程安全,性能又高,这种写法最为常用
1 /** 2 * 单例模式第四种写法:线程安全,性能又高,也是最为常用的 3 */ 4 package thread02.singleton; 5 6 import java.util.concurrent.locks.ReentrantLock; 7 8 public class Singleton04 9 { 10 private static Singleton04 instance; 11 private static ReentrantLock lock = new ReentrantLock(); 12 13 private Singleton04() 14 { 15 16 } 17 18 public static Singleton04 getSingleton() 19 { 20 if(instance == null) 21 { 22 lock.lock(); 23 24 if(instance == null) 25 { 26 instance = new Singleton04(); 27 } 28 29 lock.unlock(); 30 } 31 32 return instance; 33 } 34 }
单例模式第四种写法:线程安全,性能又高,也是最为常用的
原文地址:https://www.cnblogs.com/kehuaihan/p/8458430.html