什么是线程安全?
保证线程安全需要保证几个基本特性:
- 原子性:相关操作不会被其他线程所打扰,一般通过同步机制实现。
- 可见性:一个线程修改了某个共享变量,其状态能够立即被其他线程知晓。通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。
- 有序性:保证线程内的串行语义,避免指令重排等。
线程安全解决办法?
内置的锁(synchronized)
Java提供了一种内置的锁(Intrinsic Lock)机制来支持原子性,每一个Java对象都可以用作一个实现同步的锁,称为内置锁,也叫隐式锁。
线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁。
内置锁为互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁。
synchronize是可重入锁。
内置锁使用synchronized关键字实现,synchronized关键字有两种用法:
1.同步方法
在方法上修饰synchronized 称为同步方法,此时充当锁的对象为调用同步方法的对象。
非静态同步函数使用this锁。
//非静态synchronized修饰方法 使用的是this锁
private synchronized void sale() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次
System.out.println(Thread.currentThread().getName() + "售出第 " + (100-ticketCount+1) + "张票");
ticketCount--;
}
静态同步函数
方法上加上static关键字,使用synchronized关键字修饰或者使用类.class文件。
静态的同步函数使用的锁是该函数所属字节码文件对象
可以用getClass方法获取,也可以用当前类名.class 表示。
//static synchronized == synchronized (DeadLockDemo.class) 锁为当前的字节码文件
private static synchronized void sale() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次
System.out.println(Thread.currentThread().getName() + "售出第 " + (100-ticketCount+1) + "张票");
ticketCount--;
}
2.同步代码块
synchronize(任意全局变量){需要被同步的代码}
和直接使用synchronized修饰需要同步的方法是一样的,但是锁的粒度可以更细,并且充当锁的对象不一定是this,也可以是其它对象,所以使用起来更加灵活。
synchronized (obj) {
if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次
System.out.println(Thread.currentThread().getName() + "售出第 " + (100 - ticketCount + 1) + "张票");
ticketCount--;
}
上例中使用Object充当锁对象。
synchronized(this){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次
System.out.println(Thread.currentThread().getName() + "售出第 " + (100-ticketCount+1) + "张票");
ticketCount--;
}
上例中使用this锁。同非静态同步函数
注意: 保证多线程同步时必须保证多个线程使用同一个锁 。
3.底层实现
synchronized代码块是由一对monitorenter/monitorexit指令实现的,Monitor对象是同步的基本实现单元。
JVM提供了三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁。
4.锁的升级降级
所谓锁的升级降级,就是JVM优化synchronized运行的机制,当JVM检测到不同的竞争状态状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
没有竞争出现的时候,默认会使用偏斜锁。JVM会利用CAS操作(compare and swap),在对象头上的Mark word部分设置线程ID,以表示对象偏向于当前线程,所以不涉及真正的互斥锁。
这样做的假设是基于在很多应用场景中,大部分对象生命周期最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏离过的对象,JVM就要撤销(revoke)这个对象的偏斜锁,并切换到轻量级锁实现。轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,则使用普通的轻量级锁;否则,进一步升级为重量级锁。
当JVM进入安全点(safepoint)的时候,会检查是否有闲置的Monitor,然后试图降级。
偏斜锁、轻量级锁、重量级锁的代码实现并不在核心类库中,而是在JVM的代码中。
关闭偏斜锁:
-XX:-UseBiasedLocking
5.常见问题
不要使用String常量加锁
注意:不要使用String常量加锁,会引起死循环的问题。
public class StringLock { public void method(){ //使用字符串常量加锁 只进入t1 synchronized ("lock") //使用字符串常量加锁 如下则没有问题 synchronized (new String("lock")) { try { while (true) { System.out.println(Thread.currentThread().getId()+"------thread start--------"); Thread.sleep(1000); System.out.println(Thread.currentThread().getId()+"------thread end----------"); } } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args){ StringLock stringLock = new StringLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { stringLock.method(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { stringLock.method(); } }); t1.start(); t2.start(); } }
不要修改锁
可以修改其成员变量,但不要修改其引用。修改了引用就会释放锁。
显示锁(Lock)
Lock是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有的加锁和解锁操作方法都是显示的,因而称为显示锁。
下面我们来分析Lock的几个常见的实现类ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock。
重入锁 ReentrantLock
当一个线程试图获取一个他已经获取的锁的时候,这个获取的动作自动成功。这是一个获取锁粒度的概念,也就是锁的持有以线程为单位,并不基于调用次数。
编码中需要注意必须要明确调用unlock()方法释放,不然就会一直持有该锁。
重入锁可设置公平性(默认为非公平)
ReentrantLock fairLock = new ReentrantLock(true);
fairLock.lock();
try{
//do something
}finally {
fairLock.unlock();
}
若使用Synchronized则无法进行公平性选择。它永远都是非公平的。
若要保证公平性则会引入额外开销,会导致一定的吞吐量下降,因此只有程序确实有公平性需要的时候才有必要指定它。
读写锁 ReentrantReadWriteLock
ReadWriteLock(读写锁)是一个接口,提供了readLock和writeLock两种锁的操作,也就是说一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。也就是说读写锁应用的场景是一个资源被大量读取操作,而只有少量的写操作。我们先看其源码:
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); } |
从源码看出,ReadWriteLock借助Lock来实现读写两个锁并存、互斥的机制。每次读取共享数据就需要读取锁,需要修改共享数据就需要写入锁。
读写锁的机制:
1、读-读不互斥,读线程可以并发执行;
2、读-写互斥,有写线程时,读线程会堵塞;
3、写-写互斥,写线程都是互斥的。
举栗子:
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
final Queue3 q3 = new Queue3();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
q3.get();
}
}
}).start();
}
for (int i = 0; i < 3; i++) {
new Thread() {
public void run() {
while (true) {
q3.put(new Random().nextInt(10000));
}
}
}.start();
}
}
}
class Queue3{
private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void get(){
rwl.readLock().lock();//上读锁,其他线程只能读不能写
System.out.println(Thread.currentThread().getName() + " be ready to read data!");
try {
Thread.sleep((long)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "have read data :" + data);
rwl.readLock().unlock(); //释放读锁,最好放在finnaly里面
}
public void put(Object data){
rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写
System.out.println(Thread.currentThread().getName() + " be ready to write data!");
try {
Thread.sleep((long)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + " have write data: " + data);
rwl.writeLock().unlock();//释放写锁
}
}
模拟写一个缓存器
/**
* 使用ReentrantReadWriteLock模拟一个缓存器
* Created by zhanghaipeng on 2018/10/24.
*/
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CacheDemo {
private Map<String, Object> map = new HashMap<String, Object>();//缓存器
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
}
public Object get(String id){
Object value = null;
rwl.readLock().lock();//首先开启读锁,从缓存中去取
try{
value = map.get(id);
if(value == null){ //如果缓存中没有释放读锁,上写锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try{
if(value == null){
value = "aaa"; //此时可以去数据库中查找,这里简单的模拟一下
}
}finally{
rwl.writeLock().unlock(); //释放写锁
}
rwl.readLock().lock(); //然后再上读锁
}
}finally{
rwl.readLock().unlock(); //最后释放读锁
}
return value;
}
}
Lock与synchronized 的比较
Synchronized是在JVM层面上实现的,无需显示的加解锁,而ReentrantLock和ReentrantReadWriteLock需显示的加解锁,一定要保证锁资源被释放;
Synchronized是针对一个对象的,而ReentrantLock和ReentrantReadWriteLock是代码块层面的锁定;
ReentrantReadWriteLock和ReentrantLock的比较:
ReentrantReadWriteLock是对ReentrantLock的复杂扩展,能适合更加复杂的业务场景,ReentrantReadWriteLock可以实现一个方法中读写分离的锁的机制。而ReentrantLock只是加锁解锁一种机制。
ReentrantReadWriteLock引入了读写和并发机制,可以实现更复杂的锁机制,并发性相对于ReentrantLock和Synchronized更高。
Volatile
可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。
在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。
public class VolatileDemo implements Runnable {
// private Boolean flag = true;
//需添加volatile关键字
private static volatile Boolean flag = true;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始");
while (flag)
{
System.out.println("线程执行,flag为"+flag);
}
System.out.println(Thread.currentThread().getName() + "线程结束");
}
public void setRunning(Boolean flag){
this.flag = flag ;
System.out.println("setRunning flag -->" + flag);
}
public static void main(String[] args){
VolatileDemo volatileDemo = new VolatileDemo();
Thread t1 = new Thread(volatileDemo,"t1");
t1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileDemo.setRunning(false);
}
}
上例中的flag需要被volatile修饰后才能保证其线程间可见。
Volatile 保证了线程间共享变量的及时可见性,但不能保证原子性
对异常的处理
根据实际业务选择是否释放锁:
方法一:记录日志并继续
方法二:抛出异常
public class SynchronizedException { public synchronized void print() { int i = 0 ; while (true) { i++; System.out.println(i); try { Thread.sleep(500); if(i==5) { Integer.parseInt("a"); } } catch (Exception e) { e.printStackTrace(); //方法一:记录日志&contiunue System.out.println(Thread.currentThread().getId()); // continue; //方法二:抛出RuntimeException() throw new RuntimeException(); } } } public static void main(String[] args){ final SynchronizedException synchronizedException = new SynchronizedException(); Thread t = new Thread(new Runnable() { @Override public void run() { synchronizedException.print(); } }) ; t.start(); } }