1、死锁
概念:由于两个或多个线程互相持有对方需要的资源,导致这些线程处于等待状态,无法继续执行。
死锁原因:当前线程拥有其他线程需要的资源、当前线程等待其他线程已拥有的资源、都不放弃自己拥有的资源。
如何避免死锁:
- 固定加锁顺序:很有效,但是需要事先知道所有可能会用到的锁,但总有时候是无法预知的。
- 加锁时限:尝试获取锁时加一个超时时间,超时后该线程就放弃对该锁的请求,回退并释放所有已获得的锁,然后等待一段时间再重试,适合少量线程。
- 死锁检测:更好的死锁预防机制,主要针对不可能实现按序加锁和锁超时的场景。
jps命令(java自带)查看当前运行的java进程。jstack -l 进程id:查看进程的死锁。
2、线程中锁的种类
- 公平锁:多线程按照申请锁的顺序来获取锁,先来后到。比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程会获得锁。
- 非公平锁:无法保证按照申请顺序获得锁,有可能造成优先级反转或饥饿现象。但是吞吐量比公平锁大。
Synchronized、ReentrantLock都是非公平锁,ReentrantLock可以在构造时传入参数true,就是公平锁。
- 可重入锁:同一个线程在外层方法获得锁,进入内层方法会自动获得锁。如果不能重入,线程已经拥有了这个锁,又再申请这个锁,永远也达不到锁,可重入锁一定程度可以避免死锁。
Synchronized、Lock都是可重入锁。
- 自旋锁:线程在没有取得锁的时候不被挂起,而是去执行空循环(即自旋),若干个空循环后,该线程如果可以获得锁,则继续执行,否则才会被挂起。优点:减少线程上下文切换的消耗,缺点:循环会消耗CPU。适用:锁竞争不是很激烈,锁占用时间很短的并发线程。
自旋锁开启与自旋次数:由虚拟机自动开启并自动调整次数。
- 读写锁:区分读锁和写锁,允许多个读,只允许一个写操作,提高了执行效率。
读写锁接口:ReadWriteLock,实现类:ReentrantReadWriteLock。lock.readLock()获取读锁,lock.writeLock()获取写锁。
- 可中断锁:Synchronized锁不可中断,lock锁可以中断等锁的行为。
- 独享锁/共享锁:大部分都是独享锁,ReadWriteLock的读锁就是共享锁。
- 分段锁:不是一种锁的设计,ConcurrentHashMap通过分段锁实现高效的并发操作。
3、Volatile 关键字
- 只能修饰变量,变量的值可能随时会被别的线程修改,而且会强制将修改的值立即写入内存,所以每个线程得到的都是volatile最新的值。
- 可见性(每个线程可以立刻读取到其他线程修改后的值)、不具备原子性。
不具备原子性是与synchronized、lock最大的区别。某一线程从主内存获取到共享变量的值,当其修改完变量值重新写入主内存时,并没有去判断主内存的值是否发生改变
- synchronized 关键字可以保证变量原子性和可见性。
- 最适合一个线程写、多个线程读的场景。
4、HashMap在多线程环境下的使用
线程不安全。
两个或者多个 rehash 可能会导致 map 中同一个 buket 下链表数据形成一个环,这时再调用get方法就会出现死循环。
最好使用HashTable(效率极低)或ConcurrentHashMap。
5、守护线程(Daemon)
- 概念:用于为系统中的其他对象和线程提供服务,在没有用户线程时自动离开。将一个用户线程设置为守护线程的方法:在线程对象创建之前调用线程对象的setDaemon(true)方法。
- 典型的守护线程:垃圾回收线程。当程序中没有任何的Thread,就不会再产生垃圾,虚拟机自动离开。
- 守护线程中产生的线程也是守护线程,用户线程一样。
- 使用场景:为其他线程提供服务支持。执行一些后台任务,程序退出时,线程自动关闭。
5、ThreadLocal
- 概念:线程局部变量。只在线程的生命周期内有效,采用空间换时间。
- 原理:ThreadLocal中有一个静态内部类ThreadLocalMap,用来存变量,key为当前ThreadLocal对象,value为存入的值。
- 方法:
ThreadLocal threadLocal = new ThreadLocal();
//底层是调用:Thread t = Thread.currentThread();
//ThreadLocalMap map = t.threadLocals;
threadLocal.set("222");
//底层是调用:Thread t = Thread.currentThread();
//ThreadLocalMap map = t.threadLocals;
//map.set(threadLocal, "222");
Object obj = threadLocal.get();
//底层是调用:Thread t = Thread.currentThread();
//ThreadLocalMap map = t.threadLocals;
//ThreadLocalMap.Entry e = map.getEntry(threadLocal);
//Object obj = e.value;
注意:ThreadLocalMap 是使用 ThreadLocal 的弱引用作为key,GC 时 key 被回收,无法再访问 key 为 null 的 value,如果 value 是较大对象而且使用线程池,此时线程不退出,容易导致OOM。所以线程逻辑代码结束时,必须调用ThreadLocal.remove() 。
6、多线程上下文切换
- 概念:CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
即使单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片很短,一般几十毫秒,让我们感觉同时执行。
- 多线程不一定快:并发不超过百万时,串行执行比多线程快。因为线程有创建和上下文切换的开销。
- 减少上下文切换,提高多线程运行效率:
- 无锁并发编程:多线程竞争锁,会引起上下文切换。可以按照数据ID的Hash值分段,不同线程处理不同段的数据。
- CAS算法:JAVA的Atomic包使用CAS算法来更新数据,不需要加锁。
- 使用最少线程:避免创建不需要的线程。
- 协程:在单线程里实现多任务调度,并维持多个任务的切换。
7、可以运行时kill掉一个线程吗?可以,但是不建议
- 舍弃stop方法:强制终止线程,就像突然关闭计算机电源,线程的资源来不及释放,容易产生数据不完整。
- 使用共享变量
public class ThreadFlag extends Thread {
private volatile boolean exit = false;
@Override
public void run() {
while (!exit);
}
public static void main(String[] args) throws InterruptedException {
ThreadFlag threadFlag = new ThreadFlag();
threadFlag.start();
sleep(3000);
threadFlag.exit = true;
threadFlag.join();
System.out.println("线程退出");
}
}
- 使用thread.interrupt(),线程阻塞时退出阻塞状态,抛出InterruptedException异常。
8、线程串行执行
- thread.join():需要等待上一个线程(也就是调用此方法的thread)执行完以后才能执行当前线程。
public class ThreadByOrder {
public static void main(String[] args){
thread1.start();
thread2.start();
thread3.start();
}
static Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行:" + Thread.currentThread().getName());
}
});
static Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
thread1.join();//需要等待thread1执行结束以后才开始执行当前thread2线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行:" + Thread.currentThread().getName());
}
});
static Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
try {
thread2.join();//需要等待thread2执行结束以后才开始执行当前thread3线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行:" + Thread.currentThread().getName());
}
});
}
- CountDownLatch
public class ThreadTest2 {
//实现线程t1、t2、t3依次执行
public static void main(String[] args){
CountDownLatch c0 = new CountDownLatch(0); //t1计数器为0
CountDownLatch c1 = new CountDownLatch(1); //t2计数器为1
CountDownLatch c2 = new CountDownLatch(1); //t3计数器为1
Thread t1 = new Thread(new Worker(c0, c1));//c0为0,t1可以执行。t1的计数器减1
Thread t2 = new Thread(new Worker(c1, c2));//t1的计数器为0时,t2才能执行。t2的计数器c2减1
Thread t3 = new Thread(new Worker(c2, c2));//t2的计数器c2为0时,t3才能执行
t1.start();
t2.start();
t3.start();
}
static class Worker implements Runnable {
CountDownLatch c1;
CountDownLatch c2;
public Worker(CountDownLatch c1, CountDownLatch c2) {
super();
this.c1 = c1;
this.c2 = c2;
}
@Override
public void run() {
try {
c1.await();
System.out.println("Thread start" + Thread.currentThread().getName());
c2.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}