首先我们应该先来了解什么是线程安全问题?
给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,当多个线程同时访问,修改同一资源时,可能会造成数据异常问题,就会造成线程安全问题。因此,Java 提供了同步机制,以控制对共享资源的互斥访问。
线程同步:
就是多个线程按一定顺序执行;
即多个线程排成队列,当拿到锁资源的线程就开始执行;
线程同步的实现可以借助Synchronized关键字,ReentrantLock类,ThreadLocal类。
synchronized:
先来看看synchronized,它是Java中的关键字,是一种同步锁。
下面我们从它修饰的对象、作用以及原理这几个方面来学习它。
它修饰的对象有以下几种:
-
修饰一个代码块,被修饰的代码块称为同步语句块,其中锁的值即监听对象必须是唯一标识,可以是String类型、final修饰的、在常量池里的、所有线程对象共享的、当前类的字节码对象;其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
-
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
-
修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
-
修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。
作用:
-
确保线程互斥的访问同步代码
-
保证共享变量的修改能够及时可见
-
有效解决重排序问题。
原理:
在编译的字节码中加入了两条指令来进行代码的同步。
monitorenter :
每个对象都有一个监视器锁(monitor)。
当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
-
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
-
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
-
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程就退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
ReentrantLock类:
ReentrantLock实现了Lock 接口(JUC包下的),并提供了与synchronized相同的互斥性和内存可见性。但相较于synchronized提供了更高的处理锁的灵活性。
使用:
当然需要创建ReentrantLock对象,new ReentrantLock();
主要是3个方法:
lock():在核心代码块处加锁即获取锁,如果锁被占用,则一直等待。
unlock():解锁,一般放在finally中。
tryLock(): 尝试去获取锁;返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true。
tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间。
使用例子:
class Ticket implements Runnable{
private int tick = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock(); //上锁
try{
if(tick > 0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " 完成售票,余票为:" + --tick);
}
}finally{
lock.unlock(); //释放锁
}
}
}
}
synchronized与Lock的区别:
(1)Lock的加锁和解锁都是由java代码实现的,而synchronize的加锁和解锁的过程是由JVM管理的。
(2)synchronized能锁住类、方法和代码块,而Lock是块范围内的。
(3)Lock能提高多个线程读操作的效率;
(4)Lock:Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。底层主要靠volatile和CAS乐观锁操作实现的。
总结就是:加锁比同步更灵活,效率更高;同步是jvm级别的而锁是代码级别的。
ThreadLocal类:
概述:
ThreadLocal 是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
介绍:
ThreadLocal 叫做线程本地变量,也有些地方叫做线程本地存储。ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
Synchronized 用于线程间的数据共享,而 ThreadLocal 则用于线程间的数据隔离:多个线程操作同一个变量且互不干扰的场景下,可以使用ThreadLocal解决。
ThreadLocal其实是一个线程容器,初始容量16,负载因子2/3,解决冲突的方法是再hash法,也就是:在当前hash的基础上再自增一个常量进行哈希。
底层实现原理:
ThreadLocal存值,其实是通过 ThreadLocalMap来实现的。
ThreadLocalMap中用于存储数据的entry定义,使用了弱引用,可能造成内存泄漏。
当线程没有结束,但是ThreadLocal对象已经被回收,则可能导致线程中存在ThreadLocalMap的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。
解决办法:
(1)使用完线程共享变量后,显式调用ThreadLocalMap.remove方法清除线程共享变量;
(2)ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。
使用:
ThreadLocal类提供的几个方法:
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,
set()用来设置当前线程中变量的副本,
remove()用来移除当前线程中变量的副本,
initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。
每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。这里可以参考看一下源码:
public void set(T value) {
Thread t = Thread.currentThread();
//getMap():就是初始化threadLocals
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
需要注意的就是当给线程绑定一个 Object 内容后,只要线程不变,可以随时取出;改变线程,无法取出内容。
示例:
public class ThreadLocalTest2 {
private static int a = 500;
public static void main(String[] args) {
new Thread(()->{
ThreadLocal<Integer> local = new ThreadLocal<Integer>();
while(true){
local.set(++a); //子线程对a的操作不会影响主线程中的a
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程:"+local.get());
}
}).start();
a = 22;
ThreadLocal<Integer> local = new ThreadLocal<Integer>();
local.set(a);
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程:"+local.get());
}
}
}
//子线程:23
//主线程:22
//主线程:22
//子线程:24
//主线程:22
//子线程:25
在实现线程同步的时候,我们要注意不能造成死锁的现象。
所谓死锁就是:多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些线程都将无法向前推进。
所以在实现线程同步时,我们要避免死锁现象。方法有:
-
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)。
-
避免不要再同一代码块中,同时持有多个对象的锁。
-
使用Lock时,一定要记得释放锁。
关注公众号,可以免费获取毕业设计项目、各种免费软件、资料,笔记哦。