网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
- 同一个实例对象中的不同方法都加上synchronized关键字时,争抢的时同一个monitor的lock。
死锁原因
- 交叉锁:线程A持有R1的锁等待R2的锁,线程B持有R2的锁等待R1的锁。
- 内存不足:两个线程都在等待彼此能够释放内存资源。
- 一问一答式数据交换:客户端和服务器端都在等待双方发送数据。
- 数据库锁:无论表锁、行锁,某个线程执行for update语句退出事务,其他线程访问该数据库都会陷入死锁。
- 文件锁:某线程获得文件锁意外退出,其他线程进入死锁直到系统释放文件句柄(Handle)资源。
- 死循环:死循环造成的死锁一般成为系统假死。
线程间通信
同步阻塞和异步阻塞
同步阻塞消息处理缺点:客户端等待时间过长会陷入阻塞;吞吐量不高;频繁创建开启与销毁;业务高峰系统性能低。
异步非阻塞消息处理:优势明显,但也存在缺陷,如客户端再次调用接口方法仍然需要进行查询(可通过异步回调接口解决)。
单线程间通信
服务器端与客户端通过事件队列进行通信的case比较好的方式就是使用通知机制:创建一个事件队列,有事件则通知工作线程开始工作,没有则工作线程休息并等待通知。下面就是这样的case。
事件队列:
package com.hust.zhang.conn;
import java.util.LinkedList;
import static java.lang.Thread.currentThread;
public class EventQueue {
private int max;
public EventQueue(int num) {
this.max = num;
}
public EventQueue() {
this(DEFAULT_MAX_EVENT);
}
//object类是所有类的父类
static class Event {
}
private final LinkedList<Event> eventQueue = new LinkedList<>();
private final static int DEFAULT_MAX_EVENT = 10;
public void offer(Event event) {
synchronized (eventQueue) {
//当共享资源eventQueue队列达到上限,调用eventQueue的wait方法使当前线程进入wait set中并释放monitor的锁
if (eventQueue.size() >= max) {
try {
console("the queue is full.");
/**
* wait方法:
* 1、可中断,一旦调用wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断。
* 2、执行某个对象的wait方法后,加入与之对应的wait set中,每一个对象的monitor都有一个与之关联的wait set。
* 3、必须在同步方法中使用wait和notify,因为执行wait和notify前提条件是必须持有同步方法的monitor所有权。否则会出现IllegalMonitorStateException。
* */
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console("the event is submitted");
eventQueue.addLast(event);
eventQueue.notify();
}
}
public Event take() {
synchronized (eventQueue) {
if (eventQueue.isEmpty()) {
try {
console("the queue is empty");
//eventQueue是Event类的集合,调用的是父类Object的wait方法
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeFirst();
//notify唤醒在此对象监视器monitor上等待的单个线程
this.eventQueue.notify();
console("the event " + event + " is handled.");
return event;
}
}
private void console(String message) {
System.out.printf("%s:%s\n", currentThread().getName(), message);
}
}
模拟服务者和消费者的两个线程:
package com.hust.zhang.conn;
import java.util.concurrent.TimeUnit;
public class EventClient {
public static void main(String[] args) {
final EventQueue eventQueue = new EventQueue();
new Thread(() -> {
for (; ; ) {
eventQueue.offer(new EventQueue.Event());
}
}, "Producer").start();
new Thread(() -> {
for (; ; ) {
eventQueue.take();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Consumer").start();
}
}
多线程通信
上面的case中Producer很快提交了10个Event数据,此时队列已满,然后执行eventQueue的wait方法进入阻塞状态,Consumer线程由于要处理数据,花费1秒处理其中的一条数据,然后通知Producer线程可以继续提交数据了,如此循环。
但是上面的case如果有多个线程同时take或offer上面的程序就会出现数据不一致的问题,当eventQueue元素为空时,两个线程执行take方法分别调用wait方法进入阻塞,另一个offer线程执行addLast方法后唤醒了其中一个阻塞的线程,该线程顺利消费了一个元素之后恰巧再次唤醒了一个take线程,这时导致执行空LinkedList的removeFirst方法。所以再在上面做了一定的优化,判断eventQuque队列满或空变成了轮询队列条件(if -> while),唤醒在此对象监视器monitor等待的单个线程变成唤醒在此对象监视器monitor等待的所有线程(notify -> notifyAll)。这样改进可以防止多个线程同时take或offer造成的线程安全问题。
自定义显式锁BooleanLock
synchronized提供的是一种排他式的数据同步机制,某个线程在获取monitor lock的时候可能会被阻塞,而这种阻塞有两个很明显的缺陷:
- 无法控制阻塞时长。
- 阻塞不可被中断。
下面是一个缺陷分析的case。
package com.hust.zhang.synchronizedAnalysis;
import java.util.concurrent.TimeUnit;
public class SynchronizedDefect {
public synchronized void syncMethod() {
try {
//阻塞时间长无法控制
TimeUnit.HOURS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDefect defect = new SynchronizedDefect();
Thread t1 = new Thread(defect::syncMethod, "T1");
//make sure the t1 start
t1.start();
TimeUnit.MICROSECONDS.sleep(2);
//T2因争抢monitor的锁而进入阻塞状态,无法中断
Thread t2 = new Thread(defect::syncMethod, "T2");
t2.start();
//虽然可以设置中断标识,但是无法被中断
TimeUnit.MICROSECONDS.sleep(2);
t2.interrupt();
System.out.println("t2.isInterrupt: " + t2.isInterrupted()); //true
System.out.println("t1.state: " + t1.getState()); //TIMED_WAITING
System.out.println("t2.state: " + t2.getState()); //BLOCKED
}
}
上面的case可以看到线程t2因为争抢monitor的锁而进入阻塞状态,对其调用interrupt方法只会设置中断标识,线程一直处于阻塞状态无法被中断。但如果是休眠中的线程(Thread.sleep),调用interrupt方法会中断该线程并抛出InterruptException异常。
所以这里采用自定义显式锁BooleanLock,demo如下,
锁接口:
package com.hust.zhang.synchronizedAnalysis;
import java.util.List;
import java.util.concurrent.TimeoutException;
public interface Lock {
//永远阻塞,除非获取到了锁,方法可以被中断
void lock() throws InterruptedException;
//增加超时功能
void lock(long mills) throws InterruptedException, TimeoutException;
//锁的释放
void unlock();
//获取当前哪些线程被阻塞
List<Thread> getBlockedThreads();
}
自定义显式锁实现类:
package com.hust.zhang.synchronizedAnalysis;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
public class BooleanLock implements Lock {
//当前拥有锁的线程
private Thread currentThread;
//boolean开关,true代表该锁被某个线程获得,false代表当前锁没有被哪个线程获得或者已经释放
private boolean locked = false;
//存储哪些线程在获取当前线程时进入阻塞状态
private final List<Thread> blockedList = new ArrayList<>();
@Override
public void lock() throws InterruptedException {
//同步代码块
synchronized (this) {
//当前锁被某线程获得,则该线程加入阻塞队列,并使当前线程wait释放对this monitor的所有权
while (locked) {
blockedList.add(currentThread());
this.wait();
}
//如果当前线程没有被其他线程获得,则该线程会从阻塞队列中删除自己(如未进入阻塞队列删除也不会有影响)
blockedList.remove(currentThread());
//locked开关设为true
this.locked = true;
//记录获取锁的线程
this.currentThread = currentThread();
}
}
@Override
public void lock(long mills) throws InterruptedException, TimeoutException {
//同步代码块
synchronized (this) {
//如果mills不合法,则默认调用lock方法,抛出异常也是一个比较好的做法
if (mills <= 0) {
this.lock();
} else {
long remainingMills = mills;
long endMills = currentTimeMillis() + remainingMills;
while (locked) {
//如果remainingMills<=0,则表示当前线程被其他线程唤醒或者在指定的wait时间到之后还没有获得锁
if (remainingMills <= 0) throw new TimeoutException("can not get the lock during " + mills);
if (!blockedList.contains(currentThread)) blockedList.add(currentThread());
//等待remainingMills的毫秒数,该值最开始由其他线程传入,但多次wait过程中会重新计算
this.wait(remainingMills);
//重新计算remainingMills
remainingMills = endMills - currentTimeMillis();
}
//获得该锁,并且从block队列中删除当前线程,将locked的状态设置为true,并且指定获得锁的线程就是当前线程
blockedList.remove(currentThread());
this.locked = true;
this.currentThread = currentThread();
}
}
}
@Override
public void unlock() {
synchronized (this) {
//判断当前线程是否为获取锁的那个线程,只有加了锁的线程才有资格进行解锁
if (currentThread == currentThread()) {
this.locked = false;
//Optional类是一个可以为null的容器对象。ifPresent方法可以接受接口段或lambda表达式
Optional.of(currentThread().getName() + "release the lock.").ifPresent(System.out::println);
//通知其他在wait set中的线程,大家可以尝试抢锁了
this.notifyAll();
}
}
}
@Override
public List<Thread> getBlockedThreads() {
//重构收发Encapsulate Collection(封装集群)将参数中的List返回一个不可修改的List
return Collections.unmodifiableList(blockedList);
}
}
测试类:
package com.hust.zhang.synchronizedAnalysis;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.ThreadLocalRandom.current;
public class BooleanLockTest {
private final Lock lock = new BooleanLock();
public void synMethod() throws InterruptedException {
lock.lock();
try {
int randomInt = current().nextInt(10);
System.out.println(currentThread() + "get the lock.");
TimeUnit.SECONDS.sleep(randomInt);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
BooleanLockTest test = new BooleanLockTest();
IntStream.range(0, 10)
.mapToObj(i -> new Thread(() -> {
try {
test.synMethod();
} catch (InterruptedException e) {
e.printStackTrace();
}
}))
.forEach(Thread::start);
}
}
ThreadGroup
默认情况下,新的线程都会被加入到main线程的group中。
ThreadGroup currentGroup = Thread.currentThread().getThreadGroup();
ThreadGroup group1 = new ThreadGroup("Group1");
System.out.println(group1.getParent() == currentGroup); //true
ThreadGroup group2 = new ThreadGroup(group1, "Group2"); //true
System.out.println(group2.getParent() == group1);
ThreadGroup中的enumerate方法会将ThreadGroup中的active线程全部复制到Thread数组中。
package com.hust.zhang.threadGroup;
import java.util.concurrent.TimeUnit;
public class ThreadGroupEnumerateThreads {
public static void main(String[] args) throws InterruptedException {
ThreadGroup myGroup = new ThreadGroup("MyGroup");
Thread thread = new Thread(myGroup, () -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "MyThread");
thread.start();
TimeUnit.MICROSECONDS.sleep(2);
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
Thread[] list = new Thread[mainGroup.activeCount()];
/**
* enumerate方法获取的线程仅仅是预估值,并不能百分之百的保证当前group的活跃线程数,
* 比如在调用复制之后,某个线程结束了生命周期或者新的线程加入进来,都会导致数据的不准确。
*/
int recurseSize = mainGroup.enumerate(list);
System.out.println("主线程组活跃线程数 = " + recurseSize); //3
//递归recurse设置为false,myGroup中的线程不会包含在内
recurseSize = mainGroup.enumerate(list, false); //2
System.out.println(recurseSize);
}
}
enumerate也可以复制ThreadGroup线程组,如下
package com.hust.zhang.threadGroup;
import java.util.concurrent.TimeUnit;
public class ThreadGroupEnumerateThreadGroup {
public static void main(String[] args) throws InterruptedException {
ThreadGroup myGroup1 = new ThreadGroup("MyGroup1");
ThreadGroup myGroup2 = new ThreadGroup(myGroup1, "MyGroup2");
TimeUnit.MICROSECONDS.sleep(2);
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
ThreadGroup[] list = new ThreadGroup[mainGroup.activeGroupCount()];
int recurseSize = mainGroup.enumerate(list);
System.out.println("主线程组活跃子线程组数 = " + recurseSize); //2
recurseSize = mainGroup.enumerate(list, false);
System.out.println(recurseSize); //1
}
}
注意事项:
- 后加入到该线程组ThreadGroup的线程Thread的最大优先级不会高于线程组ThreadGroup的最大优先级。
- ThreadGroup的interrupt方法会使该group的所有active线程都被interrupt。
- ThreadGroup的destroy方法只是针对一个没有任何active线程的group进行一次destroy标记。
- ThreadGroup也可以设置为守护ThreadGroup,设置ThreadGroup为daemon也不会影响线程的daemon属性。设置了daemon属性的线程组在没有任何active线程的时候会自动destroy。
package com.hust.zhang.threadGroup;
import java.util.concurrent.TimeUnit;
public class ThreadGroupBasic {
public static void main(String[] args) throws InterruptedException {
ThreadGroup group = new ThreadGroup("group1");
Thread thread = new Thread(group, () -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "thread");
thread.setDaemon(true);
thread.start();
TimeUnit.MICROSECONDS.sleep(1);
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
//活跃线程 = 3: 主线程 + 用户thread + monitor线程
System.out.println("activeCount = " + mainGroup.activeCount());
//活跃线程组 = 1: group
System.out.println("activeGroupCount = " + mainGroup.activeGroupCount());
//最大优先级:10,线程的最大优先级不能高于所在线程组的最大优先级
System.out.println("getMaxPriority = " + mainGroup.getMaxPriority());
//名称:main
System.out.println("getName = " + mainGroup.getName());
//java.lang.ThreadGroup[name=system,maxpri=10]
System.out.println("getParent = " + mainGroup.getParent());
//list方法会把主线程组中的所有的活跃线程信息全部输出到控制台,也就是System.out
mainGroup.list();
System.out.println("------------------------");
//判断当前group是不是给定group的父group。给定的group是自己本身也为true。
System.out.println("parentOf = " + mainGroup.parentOf(group)); //true
System.out.println("parentOf = " + mainGroup.parentOf(mainGroup)); //true
}
}
Hook线程以及捕获线程执行异常
Hook线程也被成为钩子。Thread类中,处理运行时异常的API总共四个:
- setUncaughtExceptionHandler方法:为某个特定线程指定UncaughtExceptionHandler。
- setDefaultUncaughtExceptionHandler方法:设置全局的UncaughtExceptionHandler。
- getUncaughtExceptionHandler方法:获取特定线程的UncaughtExceptionHandler。
- getDefaultUncaughtExceptionHandler方法:获取全局的UncaughtExceptionHandler。
UncaughtExceptionHandler是一个FuncationalInterface,只有一个抽象方法,该回调接口会被Thread中的dispatchUncaughtException方法调用。
下面就是一个UncaughtExceptionHandler的栗子,设置的回调接口将获得该异常信息并打印出来
package com.hust.zhang.hook;
import java.util.concurrent.TimeUnit;
public class CaptureThreadException {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName() + "occur exception");
e.printStackTrace();
});
final Thread thread = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里出现unchecked异常
System.out.println(1 / 0);
}, "Test-thread");
thread.start();
}
}
Hook线程实战:在开发中为了防止某个程序被重复启动,在进程启动的时候创建一个lock文件,进程收到中断信息的时候会删除这个lock文件。在mysql服务器、zookeeper、kafka等系统中都能看到lock文件的存在。下面模拟一个防止重复启动的程序。
package com.hust.zhang.hook;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class PreventDuplicated {
private final static String LOCK_PATH = "/Users/kaizhang/workspace/hust-zhang/locks";
private final static String LOCK_FILE = ".lock";
![img](https://img-blog.csdnimg.cn/img_convert/7b4202e2548969a5caec5384c1837f76.png)
![img](https://img-blog.csdnimg.cn/img_convert/2feb6ba9ba30d6dfcce018ff7f156d28.png)
![img](https://img-blog.csdnimg.cn/img_convert/eaa6b892386fa9cfadd3e4e5ebd8d132.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**
Permissions;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class PreventDuplicated {
private final static String LOCK_PATH = "/Users/kaizhang/workspace/hust-zhang/locks";
private final static String LOCK_FILE = ".lock";
[外链图片转存中...(img-ByGPe5iP-1715533421748)]
[外链图片转存中...(img-BLUKUUXK-1715533421748)]
[外链图片转存中...(img-1GrTvHmc-1715533421748)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**