线程间共享
synchronized内置锁
每个线程开始运行,都会分配一个独立的运行空间–栈空间.如同一个脚本按照一行一行代码运行,直到结束,但是如果每个线程都是独立的,那就变得没什么价值,或者价值很少,但是如果多个线程进行数据共享,协同处理同一件事情,那就会带来巨大价值.
Java中支持多个线程同时访问一个对象或者对象的成员变量.
虽然多个线程同时处理一件事情给我们带来很快的速度和很高的价值,同时也带来资源竞争问题.要处理这个竞争问题,Java引入了锁概念.
synchronized修饰方法或者同步块的方式进行,确保方法或者同步块中只能有一个线程在运行,它确保了线程对变量访问的可见性和排他性.又称为内置锁,其本质就是对象锁,锁住某个对象,需要线程拿到该对象才能执行.
- 对象锁和类锁
对象锁是用于对象实例或者对象实例上的方法.类锁是用于类的静态方法或者class对象上.因为对象可以很多个,但是class只有一个.所以不同对象锁之间是互不干扰的. - 错误加锁
我们一般作为锁的对象都是一个不可变对象,比如Object,如果我们用可变对象,比如Integer,并且每次执行还会修改它的值,那锁对象就会改变. - volatile 最轻量的同步锁
使用了volatile关键字,它保证了一个线程对变量操作时候,其他线程立即可见.但是其并不会保证多线程操作变量时的原子操作.所以其称之为最轻量同步锁,适合的场景是一个线程写,多个线程读.
ThreadLocal
- 与synchronized区别:
两者有本质的区别,synchronized主要是用锁机制,使变量或代码块同一时间只能被一个线程访问.而ThreadLocal是为每个线程提供了变量副本,使每个线程在某个时间访问到的并非同个对象. - ThreadLocal场景:
在Spring的事物中,就引入了ThreadLocal,我们在请求数据库连接,执行语句,提交事务或者回滚事务都是需要数据库连接的,Spring就是在连接时候将数据库连接放在了ThreadLocal中,这样每次需要用到的时候直接拿出来就可以.
事务我们一般都是放在service层,如果将其放在DAO层,因为每个DAO层都需要用到数据库连接,但是我们调用的DAO数量是不定的,边界无法控制,而且需要传递一个连接过去.这样让工作量会大增. - ThreadLocal使用:
private static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
//设值
stringThreadLocal.set("string");
//获取值
stringThreadLocal.get();
//删除
stringThreadLocal.remove();
源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap:是属于线程内部,包含了一组Entry,Entry的key是ThreadLocal(这个是个弱引用),value是需要隔离的变量,Entry作用是有多个变量需要做隔离.
- 引发内存泄漏问题
Java中引用分为强引用,软引用,弱引用,虚引用,虚引用一般用不到,弱引用在内存回收时候,会直接回收,软引用在第一次回收时候还不会回收,只有内存空间不足时候才会继续回收,强引用只要有引用在,都不会被回收
上图可以虚线表示弱引用,当发生GC时候,ThreadLocal会被回收,那我们在Entry里面存的值是以ThreadLocal作为key的,那当ThreadLocal不存在时候,当前线程是获取不到我们存在里面的值的,只有当前线程执行完后才会被回收,这样就会发生了我们所说的线程泄漏.虽然ThreadLocal调用了expungeStaleEntry方法用来清除Entry中Key为null的Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。只有remove()方法中显式调用了expungeStaleEntry方法所以我们在使用完ThreadLocal后执行remove方法,这样就可以防止内存泄漏.
下面我们分两种情况讨论:
key 使用强引用:对ThreadLocal对象实例的引用被置为null了,但是ThreadLocalMap还持有这个ThreadLocal对象实例的强引用,如果没有手动删除,ThreadLocal的对象实例不会被回收,导致Entry内存泄漏。
key 使用弱引用:对ThreadLocal对象实例的引用被被置为null了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal的对象实例也会被回收。value在下一次ThreadLocalMap调用set,get,remove都有机会被回收。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
-
总结
JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。
JVM利用调用remove、get、set方法的时候,回收弱引用。
当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
使用线程池+ ThreadLocal时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况 -
错误使用ThreadLocal会使线程不安全
由于ThreadLocalMap的value保存的其实是变量的引用,所以我们要每个ThreadLocal都持有单独的对象.错误用法示例如下:
线程协作
线程协作是指线程之间配合工作,比如一个线程修改了值,另一个线程就去做相应的事情.前者是一个生产者,后者是一个消费者,简单的方法是让消费者不断的循环检查变量是否满足要求.但是这样会产生较大的资源开销,如果要降低资源开销,又难以保证及时性.
等待/通知机制
- 线程A调用了对象O的wait()方法,线程B调用了对象O的notify/notifyAll方法,两个线程之间通过对象来完成通讯.
- 等待通知范式:
必须要在同步方法或者同步代码块内,必须要获得锁才能执行. - wait方法:
进入wait方法后线程是会释放锁的,只有等另外一个线程通知或者被中断才会返回 - notifyAll
通知等待该对象的所有线程,不会释放锁资源.notify是随机通知一个. - 使用等待通知机制实现线程池连接
public class SelfPool {
//定义线程池
LinkedList<Connection> pool = new LinkedList<>();
//在线程池中加入线程连接
public SelfPool(int count){
for (int i=0;i<count;i++){
//SqlConnectImpl implements Connection,创建一个连接放进线程池
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
//获取线程
public Connection fetchConnection(long mills) throws InterruptedException {
//要先锁住线程池
//等待通知机制范式
synchronized (pool){
if (mills <= 0){
//判断线程池是否有线程
while (pool.isEmpty()){
pool.wait();
}
//从头开始拿
return pool.removeFirst();
}else {
//过期时间
long future = System.currentTimeMillis()+mills;
long remain = mills;
//如果池子为空,而且等待时间还有,则继续等待
while (pool.isEmpty() && remain > 0){
pool.wait(remain);
remain = future - System.currentTimeMillis();
}
Connection connection = null;
//超时出来,再判断是否有线程可用
if (!pool.isEmpty()){
connection = pool.removeFirst();
}
return connection;
}
}
}
//归还线程
public void releaseCon(Connection connection){
synchronized (pool){
pool.addLast(connection);
//放回线程,通知正在等待的连接的线程
pool.notifyAll();
}
}
}
- 调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?
1、yield():让出CPU执行权,但是不会释放资源
2、sleep():持有资源进入睡眠,不会释放
3、wait():进入阻塞,但是会释放资源,被唤醒后会继续竞争锁资源
4、notify():自身不会释放资源,但是在代码块执行完会自然而然释放资源,所以一般会放在最后执行.