线程知识总结(二)
本篇文章以线程同步的相关内容为主。线程的同步机制主要用来解决线程安全问题,主要方式有同步代码块、同步方法等。首先来了解何为线程安全问题。
1、线程安全问题
卖票示例,4 个窗口卖 100 张票:
class Ticket implements Runnable {private int total = 100;@Overridepublic void run() {while (total > 0) {// 因为 Runnable 接口中的 run() 没有 throws 任何异常,因此实现类覆盖的方法也不能抛,只能try// 根本原因是子类覆盖父类或接口所抛的异常,只能是父类方法抛的异常或其子类try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}total--;System.out.println(Thread.currentThread().getName() + "..." + total);}}
}public class Test {public static void main(String[] args) {Ticket ticket = new Ticket();Thread[] threads = new Thread[4];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(ticket);threads[i].start();}}
}
以上代码是有线程安全问题的。在 while 循环内 sleep() 后再操作 total,很容易就会出现票数为负数的情况。原因是有多个线程在操作共享的数据。
在上例中,total 变量是共享数据并且被多个线程操作了:
public void run() {while (total > 0) {// 在这里,线程执行权被切换到其它线程上try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}total--;System.out.println(Thread.currentThread().getName() + "..." + total);}}
多个线程可能执行完 while 的判断条件进入循环之后,CPU 就切换了线程,去执行其他线程了,其他线程也能进到这个循环然后执行 total–。当执行权切换回来,此时可能 total > 0 的条件已经不满足了,但是程序仍会会接着执行 total–,导致 total 变为负数。
2、synchronized
使用 synchronized 关键字构造一个同步代码块或同步方法可以有效的解决线程安全问题。实际上,相当于将 synchronized 范围内的所有代码都变成了一个原子操作来保证线程安全的。
2.1 同步代码块
使用同步代码块将可能出现线程安全的代码包起来:
class Ticket implements Runnable {private int total = 100;private Object obj = new Object();@Overridepublic void run() {// 同步代码块,任何对象都可以作为锁,比如 Ticket.class 也可以作为锁synchronized (obj) {while (total > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}total--;System.out.println(Thread.currentThread().getName() + "..." + total);}}}
}
假如4个线程中,Thread-0 拿到了执行权,那么它就会把 obj 从 1 置为 0,这样其它线程在 synchronized(obj) 处进行判断的时候就无法进入同步代码块。虽然 Thread-0 会在 sleep(100) 期间释放掉执行权,但不会释放锁,所以其它线程也是无法进入同步代码块的。等 Thread-0 完全执行完同步代码,会把 obj 从 0 置为 1,其它线程就可以争夺执行权、加锁、执行代码了。
同步解决了线程的安全问题,但因为同步锁外的线程需要等待拿到锁之后才可以执行其任务,所以相对的降低了效率。
必须要注意一下同步的使用前提,即需要同步的线程必须有使用同一个锁。例如还是刚才的例子,改一处:
class Ticket implements Runnable {private int total = 100;@Overridepublic void run() {// 把同步锁声明成方法内的局部变量Object obj = new Object();// 同步代码块synchronized (obj) {while (total > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}total--;System.out.println(Thread.currentThread().getName() + "..." + total);}}}
}
把所 obj 从成员变量变成了线程内的局部变量,这样就从 4 个线程共用 1 个 obj 锁变成了每个线程中都有一个 obj 锁,每个线程只使用自己的锁,使得同步失败。
2.2 同步方法
把 synchronized 关键字加在方法前就可以不用显式地使用对象来进行同步了,这就是同步方法。
仍然是卖票的例子,使用同步方法来做,该怎么做呢?直接这样:
class Ticket implements Runnable {private int total = 100;@Overridepublic synchronized void run() {while (total > 0) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}total--;System.out.println(Thread.currentThread().getName() + "..." + total);}}
}
简单粗暴的把 synchronized 关键字加在 run 方法前,运行程序你会发现一直都是 Thread-0 在执行,其它线程根本没有卖票!!!
这是因为 Thread-0 进入方法后,一直满足 while 循环的条件,所以它会一直循环,直到 total = 0 走出循环再结束方法。也就是说,while 循环语句并不需要同步,产生线程安全问题的代码是 while 循环体内的代码。因此这样修改:
class Ticket implements Runnable {private int total = 100;@Overridepublic void run() {while (true) {show();}}public synchronized void show() {if (total > 0) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}total--;System.out.println(Thread.currentThread().getName() + "..." + total);}}
}
把产生安全问题的代码提到一个方法中,然后用 synchronized 修饰这个方法使其变成同步方法。
我们开头提过同步方法不用像同步代码块那样显式的指定锁对象,实际上它内部还是使用了锁对象的。对于成员同步方法而言,调用该方法的对象就是锁对象,即 this;对于静态同步方法而言,该方法所在的类对象就是锁对象,即 Xxx.class。这里要注意,有可能不同的线程产生不同的对象调用各自的成员同步方法,打破了唯一锁的规则,使得同步失败,因此建议使用同步代码块。
2.3 注意事项
同步代码块一般使用可能被并发访问的共享资源充当同步锁,或者干脆使用所在的类对象 Xxx.class。
synchronized 关键字可以修饰方法和代码块,但不能修饰构造器、成员变量。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的的负面影响,程序可以采用如下策略:
- 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步。
- 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程安全版本(多线程环境中使用)和线程不安全版本(单线程环境中使用),例如 StringBuffer(安全) 和 StringBuilder(不安全)。
synchronized 同步锁的释放不是由程序显式控制的,以下介绍了哪些情况会释放锁,哪些不会:

3、线程间通信
synchronized 保证了线程安全,但是只有 synchronized 还远远不足以面对复杂的线程使用场景,比如多个线程在处理同一资源,但是任务却不同。举个例子,假设资源 Resource 有属性 count,现在有两个线程,一个去写 count,另一个读 count,要求两个线程交替执行。这个需求就需要用到线程间通信了。
3.1 wait()、notify() 与 notifyAll()
实现例子的思路:
- 在 Resource 内定义一个标记位 flag,表示当前数据是可读还是可写。
- 写线程拿到锁后,如果 Resource 可写,那么就写入数据并通知读线程;否则,进入等待状态,直到被读线程通知可以进行写操作。
- 读线程拿到锁后,如果 Resource 可读,那么就读取数据并通知写线程;否则,进入等待状态,直到被写线程通知可以进行读操作。
上述思路的实现需要用到 Object 中定义的方法:
- wait():让线程处于等待状态,被 wait() 的线程会被存储到线程池中。
- notify():随机唤醒线程池中的一个线程。
- notifyAll():唤醒线程池中的所有线程。
注意:
-
上述三个方法只能由拥有对象锁的线程调用,一个线程有三种方式拥有对象锁:
a) 执行 synchronized 修饰的对象同步方法
b) 执行 synchronized 修饰的静态同步方法
c) 执行持有该对象锁的 synchronized 同步代码块
也就是说必须在 synchronized 范围内使用,否则会抛出 IllegalMonitorStateException。
-
必须要明确到底操作的是哪个锁上的线程。只有知道了所属的锁,才能去唤醒这个锁上的其它线程,而处于等待状态的线程才能放到这个锁的线程池当中。
-
关于为什么这三个操作线程的方法被定义在 Object 类中:因为所有的对象都可以作为锁,也就是这个锁的方法存在于所有对象中,在 Java 中没有其它比 Object 这个所有类的父类更合适的定义地方了。
那么例子的实现代码可以这样:
public class ThreadCommunicationDemo1 {static class Resource {private int count;// false 可写不可读,true 可读不可写private boolean flag = false;public void setCount(int count) {this.count = count;}public int getCount() {return count;}public boolean isFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}}static class WriteThread implements Runnable {private Resource mResource;public WriteThread(Resource resource) {mResource = resource;}@Overridepublic void run() {while (true) {synchronized (mResource) {if (mResource.isFlag()) {try {// 一定要调用锁的 wait(),而不是直接调用 wait()mResource.wait();} catch (InterruptedException e) {e.printStackTrace();}}int count = new Random().nextInt(100);mResource.setCount(count);mResource.setFlag(true);System.out.println("写入数据 count = " + count);mResource.notify();}}}}static class ReadThread implements Runnable {private Resource mResource;public ReadThread(Resource resource) {mResource = resource;}@Overridepublic void run() {while (true) {synchronized (mResource) {if (!mResource.isFlag()) {try {mResource.wait();} catch (InterruptedException e) {e.printStackTrace();}}mResource.setFlag(false);System.out.println("读取数据 count = " + mResource.getCount());mResource.notify();}}}}public static void main(String[] args) {Resource resource = new Resource();new Thread(new WriteThread(resource)).start();new Thread(new ReadThread(resource)).start();}
}
虽然上述代码确实能实现要求的功能,但是实现方式却很粗糙。把等待和唤醒操作从线程移入 Resource 中会好一点:
public class ThreadCommunicationDemo2 {static class Resource {private int count;// false 可写不可读,true 可读不可写private boolean flag = false;public synchronized int getCount() {if (!flag) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}flag = false;notify();return count;}public synchronized void setCount(int count) {if (flag) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}this.count = count;flag = true;notify();}}static class WriteThread implements Runnable {private Resource mResource;public WriteThread(Resource resource) {mResource = resource;}@Overridepublic void run() {while (true) {synchronized (mResource) {int count = new Random().nextInt(100);mResource.setCount(count);System.out.println(Thread.currentThread().getName() + "写入数据 count = " + count);}}}}static class ReadThread implements Runnable {private Resource mResource;public ReadThread(Resource resource) {mResource = resource;}@Overridepublic void run() {while (true) {synchronized (mResource) {System.out.println(Thread.currentThread().getName() + "读取数据 count = " + mResource.getCount());}}}}public static void main(String[] args) {Resource resource = new Resource();new Thread(new WriteThread(resource)).start();new Thread(new ReadThread(resource)).start();}
}
3.2 多生产者与多消费者
上面的例子如果放在多生产者多消费者模型中会暴露出线程安全问题,假如我们创建两个读线程,两个写线程,运行上面的程序会得到如下输出(Thread-0、1 是写线程,Thread-2、3 是读线程):
Thread-1写入数据 count = 89 ---> 正常的部分
Thread-2读取数据 count = 89
Thread-0写入数据 count = 70 ---> 写一次读两次
Thread-2读取数据 count = 70
Thread-3读取数据 count = 70
Thread-0写入数据 count = 73
Thread-2读取数据 count = 73
Thread-3读取数据 count = 73
...
Thread-1写入数据 count = 72 ---> 写两次读一次
Thread-0写入数据 count = 7
Thread-3读取数据 count = 7
以写一次读两次的异常状况为例,从 “Thread-2读取数据 count = 89” 这一行开始分析其中原因:
- Thread-2 刚读了一次 89,那么 flag 会被置成 false,表示可以写入;
- 4 个线程争夺执行权,假设 Thread-3 拿到了执行权,由于 flag = false,不能读取,所以调用到 wait() 进入线程池等待;
- 剩余的 3 个线程争夺执行权,Thread-2 拿到了执行权,同样的原因,它也要执行 wait() 进入线程池等待;
- 这下只剩 2 个写线程争夺执行权了,Thread-0 拿到执行权,写入了数据 70,并将 flag 置为 true,最后调用 notify() 唤醒一个线程池中等待的线程;
- Thread-2 被唤醒并且抢到执行权,它会接着 wait() 后面的代码继续执行,读取到数据并且唤醒线程池中仅剩的 Thread-3;
- Thread-3 被唤醒并且抢到执行权,它也不用再做 if 判断 flag 了,也是接着执行 wait() 之后的代码,也读取到数据,从而发生了线程安全问题。
可以判断,造成问题的原因是,执行过 wait() 被唤醒的线程,没有再次判断 flag。因此可以考虑把 if(flag) 改为 while(flag),这样在线程被唤醒之后会再次在循环条件处判断 flag,不过可能会出现死锁:
- 假设 Thread-0 和 Thread-1 在线程池中 wait(),这时候进来一个消费者 Thread-2,消费并唤醒了 Thread-0。(1等待,2、3、0活)
- Thread-2 和 Thread-3 先后执行被 wait()。(1、2、3等待,0活)
- Thread-0 执行并生产一次,唤醒了 Thread-1。(0、1活,2、3等待,此时 flag 为 true了)
- Thread-0 和 Thread-1 先后执行由于 flag 为 true 结果 wait(),至此全部线程 wait() 发生死锁。
也就是说,如果出现唤醒本方的情况,就可能造成死锁。因此需要唤醒所有线程,把 notify() 换成 notifyAll()。这样在第 3 步时,就唤醒了1、2、3,消费者被唤醒了就不会死锁了。
3.3 其它线程间通信方式
通过 synchronized 配合 Object 类的 wait()、notify()、notifyAll() 三个方法是属于传统的实现线程间通信的方式。相对新兴一点的方式是下一节要介绍的 Lock 搭配 Condition 的 await()、signal()、signalAll()。
此外,使用阻塞队列(BlockingQueue)也能控制线程通信。BlockingQueue 接口作为 Queue 的子接口,主要作用不是作为容器,而是作为线程同步的工具。它有一个特征:生产者线程试图向 BlockingQueue 放入元素时,如果该队列已满,则该线程被阻塞;消费者线程试图从 BlockingQueue 中取出元素时,如果该队列已空,则该线程被阻塞。
BlockingQueue 除了可以使用 Queue 中提供的方法之外,还提供了一对儿阻塞方法 put() 和 take(),对应关系如下:

最后附上 BlockingQueue 接口的继承体系:

4、Lock 接口
Lock 是在 JDK 1.5 加入的特性,它允许实现比同步代码块和同步方法更灵活的结构,并且支持多个相关的 Condition 对象。Lock 是一个接口,定义了如下方法:

使用 Lock 接口必须显式地调用 lock()/unlock() 给临界区上锁/解锁,因此它是一个显式锁。而 synchronized 不用显式调用方法来上锁与解锁,因此 synchronized 是一个隐式锁。
4.1 ReentrantLock
ReentrantLock 是 Lock 接口最常用的实现类,翻译过来是可重入锁,顾名思义,即一个线程可以对已被加锁的 ReentrantLock 再次加锁(而不发生死锁)。ReentrantLock 对象内部会维护一个计数器来追踪 lock() 的嵌套调用,以确保程序调用 unlock() 释放锁。ReentrantLock 的基本代码框架如下:

为了确保释放锁的动作不会因为其他代码抛出异常而不被执行,通常情况下 unlock() 要在 finally 代码块中调用,这是非常重要的一点。
4.2 Condition
Condition 接口一般被称作条件对象,主要提供了 await()、signal()、signalAll() 三个方法分别对应 Object 的 wait()、notify()、notifyAll(),它将 Object 的这三个监视器方法分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用。
Lock-Condition 这套接口,与 synchronized 关键字的不同之处在于:synchronized 的锁,只能有一组属于这个锁的线程通过 wait()、notify()、notifyAll() 进行通信,而一个 Lock 上可以绑定多个 Condition,每个 Condition 里边可以用 await()、signal()、signalAll() 通信。利用这一点,可以对 3.2 节中多生产者多消费者的例子再次做出优化。
在 3.2 节中,即便是使用 notifyAll() 也是有几率唤醒本方线程的,虽然这个例子中没有造成严重的后果,但是也算是可以优化的地方。使用 Lock-Condition 可以指定唤醒对方的线程,即在 Lock 对象通过 newCondition() 生成生产者和消费者的两个 Condition 对象,那么等待/唤醒就需要指定是在哪个 Condition 上执行,获取锁的位置也要修改为等待 Lock 的哪一个 Condition:
public class ThreadCommunicationDemo4 {static class Resource {private int count;// false 可写不可读,true 可读不可写private boolean flag = false;Lock lock = new ReentrantLock();// 生产者条件final Condition producerCon = lock.newCondition();// 消费者条件final Condition consumerCon = lock.newCondition();public int getCount() {lock.lock();try {while (!flag) {try {// 消费者等待consumerCon.await();} catch (InterruptedException e) {e.printStackTrace();}}flag = false;// 唤醒生产者,不必唤醒全部producerCon.signal();} finally {lock.unlock();}return count;}public void setCount(int count) {lock.lock();try {while (flag) {try {// 生产者等待producerCon.await();} catch (InterruptedException e) {e.printStackTrace();}}this.count = count;flag = true;// 唤醒消费者,不必唤醒全部consumerCon.signal();} finally {lock.unlock();}}}static class WriteThread implements Runnable {private final Resource mResource;public WriteThread(Resource resource) {mResource = resource;}@Overridepublic void run() {while (true) {// 生产者需要拿到锁上面的“生产者条件”synchronized (mResource.producerCon) {int count = new Random().nextInt(100);mResource.setCount(count);System.out.println(Thread.currentThread().getName() + "写入数据 count = " + count);}}}}static class ReadThread implements Runnable {private final Resource mResource;public ReadThread(Resource resource) {mResource = resource;}@Overridepublic void run() {while (true) {// 消费者需要拿到锁上面的“消费者条件”synchronized (mResource.consumerCon) {System.out.println(Thread.currentThread().getName() + "读取数据 count = " + mResource.getCount());}}}}public static void main(String[] args) {Resource resource = new Resource();new Thread(new WriteThread(resource)).start();new Thread(new WriteThread(resource)).start();new Thread(new ReadThread(resource)).start();new Thread(new ReadThread(resource)).start();}
}
这样就实现了分组唤醒。
4.3 synchronized 与 Lock
下面来聊聊 synchronized 与 Lock 之间的区别与联系。
synchronized 是一个隐式锁,允许每个对象有一个内部锁,该锁有一个内部条件,这使得:
- 相对简单,编写代码相对简洁
- 每个锁仅有单一的条件,可能不够
- 不能中断一个正在等待获得锁的线程
- 不能尝试拿锁,更加不能设置尝试拿锁的超时时间
synchronized 还强制要求加锁与释放锁要出现在一个块结构中,而且当获取了多个锁时,必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。总结起来就是编程方便也能避免一些错误,但是不够灵活。
Lock 是一个显式锁,其实现类 ReentrantLock 是可重入锁,允许每个对象持有多个锁,并且每个锁可以有多个条件变量,有如下特点:
- 使用相对复杂,编写代码不够简洁,容易犯错
- 每个锁有多个条件,可以满足复杂的同步使用场景
- 提供了 lockInterruptibly() 可以中断等待获取锁的线程
- 提供了 tryLock() 和 tryLock(long time, TimeUnit unit),可以尝试获取锁,并设置超时时间
Lock 相比于 synchronized 更加灵活,Condition 也将监视器方法单独进行了封装,变成 Condition 监视器对象,可以任意锁进行组合。
关于所有同步工具使用的优先顺序:
- 最好既不使用 Lock-Condition 也不使用 synchronized 关键字。如果 java.util.concurrent 包下的机制能满足你的需求,应优先使用它们,如阻塞队列、并行流等。
- 如果 synchronized 适合你的程序应尽量使用它,这样可以减少代码量和出错几率。
- 当特别需要 Lock-Condition 结构提供的独有特性时,才使用它们。
4.4 锁的分类
通过前面的介绍我们也能发现,有多个角度可以对锁进行分类,比如前面已经说过的显式锁 Lock 与隐式锁 synchronized。除此之外,还有可重入锁。
可重入锁是指某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。除了前面提到的 ReentrantLock 外,synchronized 也是可重入锁。比如说:
public void synchronized test() {count++;test();}
不会出现死锁,原因就是 synchronized 是可重入锁。
参考文章:
Java可重入锁详解
可重入锁详解(什么是可重入)
此外,根据执行原子操作之前还是之后获得锁这一点,可以分为乐观锁和悲观锁。像 CAS 这种先进行计算后进行校验的锁称为乐观锁,而像 synchronized 这种先进行校验,没有锁就不能执行的锁,称为悲观锁。
读写锁接口 ReadWriteLock 的唯一实现类 ReentrantReadWriteLock 的性能要比普通的 同步锁高很多,原因是多线程读取并不会引发线程安全问题,因此读取锁使得所有要读取的线程都能访问到共享资源并获取最新的数据(加了读锁能获取到最新,不加读锁也可拿到数据,不过不是最新的)。读写锁适用于读取请求较多的情况,下例模拟一个购物 App 访问商品数据:
// 商品 JavaBean
public class GoodsInfo {private final String name;private double totalMoney; //总销售额private int storeNumber; //库存数public GoodsInfo(String name, int totalMoney, int storeNumber) {this.name = name;this.totalMoney = totalMoney;this.storeNumber = storeNumber;}public double getTotalMoney() {return totalMoney;}public int getStoreNumber() {return storeNumber;}// 卖出 sellNumber 件商品后更新库存和销售额public void updateStoreNumber(int sellNumber) {this.totalMoney += sellNumber * 25;this.storeNumber -= sellNumber;}
}
GoodsService 接口用来规定读取/写入商品数据:
public interface GoodsService {GoodsInfo getGoodsInfo();void setNum(int num);
}
它的两个实现类 SynService 和 RwLockService 分别使用 synchronized 同步方法和读写锁的方式实现了读写商品方法:
public class SynService implements GoodsService {private GoodsInfo goodsInfo;public SynService(GoodsInfo goodsInfo) {this.goodsInfo = goodsInfo;}@Overridepublic synchronized GoodsInfo getGoodsInfo() {try {Thread.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}return goodsInfo;}@Overridepublic synchronized void setNum(int num) {try {Thread.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}goodsInfo.updateStoreNumber(num);}
}public class RwLockService implements GoodsService {private GoodsInfo goodsInfo;private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();private final Lock getLock = readWriteLock.readLock();private final Lock setLock = readWriteLock.writeLock();public RwLockService(GoodsInfo goodsInfo) {this.goodsInfo = goodsInfo;}@Overridepublic GoodsInfo getGoodsInfo() {getLock.lock();try {Thread.sleep(5);return goodsInfo;} catch (InterruptedException e) {e.printStackTrace();return goodsInfo;} finally {getLock.unlock();}}@Overridepublic void setNum(int num) {setLock.lock();try {Thread.sleep(5);goodsInfo.updateStoreNumber(num);} catch (InterruptedException e) {e.printStackTrace();} finally {setLock.unlock();}}
}
我们还需要自定义两个线程类,一个只负责读,另一个只负责写,同时用线程对象的数量来模拟实际项目中,读请求数量大于写请求的数量:
public class BusinessApp {private static final int readWriteRatio = 10; // 读写线程的比例private static final int minThreadCount = 3; // 最少线程数private static class ReadThread implements Runnable {private GoodsService goodsService;public ReadThread(GoodsService goodsService) {this.goodsService = goodsService;}@Overridepublic void run() {long start = System.currentTimeMillis();for (int i = 0; i < 100; i++) { //操作100次goodsService.getGoodsInfo();}System.out.println(Thread.currentThread().getName() + "读取商品数据耗时:"+ (System.currentTimeMillis() - start) + "ms");}}private static class WriteThread implements Runnable {private GoodsService goodsService;public WriteThread(GoodsService goodsService) {this.goodsService = goodsService;}@Overridepublic void run() {long start = System.currentTimeMillis();for (int i = 0; i < 10; i++) { //操作10次try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}goodsService.setNum(new Random().nextInt(10));}System.out.println(Thread.currentThread().getName()+ "写商品数据耗时:" + (System.currentTimeMillis() - start) + "ms---------");}}public static void main(String[] args) {GoodsInfo goodsInfo = new GoodsInfo("Goods", 100, 100);// 内置锁
// GoodsService goodsService = new SynService(goodsInfo);// 读写锁GoodsService goodsService = new RwLockService(goodsInfo);for (int i = 0; i < minThreadCount; i++) {Thread writeThread = new Thread(new WriteThread(goodsService));for (int j = 0; j < readWriteRatio; j++) {Thread readThread = new Thread(new ReadThread(goodsService));readThread.start();}writeThread.start();}}
}
main() 中通过给 GoodsService 创建两种锁实例的方式来进行对比,可以看到使用读写锁的耗时要远远小于 synchronized 的同步锁:
// 读写锁
Thread-22写商品数据耗时:636ms---------
Thread-26读取商品数据耗时:747ms// synchronized 同步锁
Thread-2读取商品数据耗时:17050ms
Thread-11写商品数据耗时:17348ms---------
读写锁可以参考以下文章:
深入理解读写锁—ReadWriteLock源码分析

公平锁 FairSync 与非公平锁 NonFairSync 其实是 ReentrantLock 的静态内部类,并且是 final 的。创建 ReentrantLock 对象时如果在构造方法中传入 true 就会构造出一个公平锁,否则创建非公平锁:
// 默认创建非公平锁public ReentrantLock() {sync = new NonfairSync();}public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
公平锁与非公平锁:
- 公平锁:按照线程申请锁的顺序执行线程。哪个线程先申请的锁、等待的时间最长就先获得锁,后申请锁的线程会被暂时挂起等待。到了执行顺序后,再把该线程由挂起状态转换成可执行状态,这个上下文切换的过程是非常耗时的,大概需要20000个时间周期(1个时间周期就是执行1条语句所需的时间)。因此公平锁的效率要大大低于非公平锁,故默认情况下创建的是非公平锁。
- 非公平锁:线程的执行顺序与申请锁的顺序无关,全凭操作系统调度。synchronized 就是非公平锁。
听起来公平锁更合理一些,但是使用公平锁要比常规锁慢很多,并且即使使用公平锁也无法保证线程调度器是公平的。因此只有当你确实了解自己要做什么并且对你要解决的问题确实有一个特定的理由必须使用公平锁的时候,才可以使用公平锁。
5、死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁的危害很大,主要有以下三方面:
- 线程不工作了,但是整个程序还是活着的
- 没有任何的异常信息可以供我们检查
- 一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对正式已发布程序来说,这是个很严重的问题。
5.1 形成死锁的条件
同步机制使用不当可能会造成死锁,最常见的情景之一就是同步的嵌套:
public class NormalDeadLock {private static final Object lock1 = new Object();private static final Object lock2 = new Object();private static class RunnableA implements Runnable {@Overridepublic void run() {String threadName = Thread.currentThread().getName();synchronized (lock1) {System.out.println(threadName + " got lock1");try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2) {System.out.println(threadName + " got lock2");}}}}private static class RunnableB implements Runnable {@Overridepublic void run() {String threadName = Thread.currentThread().getName();synchronized (lock2) {System.out.println(threadName + " got lock2");try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock1) {System.out.println(threadName + " got lock1");}}}}public static void main(String[] args) {new Thread(new RunnableA()).start();new Thread(new RunnableB()).start();}
}
产生死锁的必要条件:
- 多个操作者(m>=2)争夺多个资源(n>=2),且 n<=m。
- 争夺资源的顺序不对。
- 拿到资源不放手。
学术化定义的死锁条件:
- 互斥:拿到资源后独占。
- 请求和保持:已经拿到了资源,还要请求新的资源。
- 不剥夺:线程已经拿到的资源,在使用完成前不能被剥夺,只能在使用完后自己释放。
- 环路等待:线程 0 拿了 A 锁请求 B 锁,而线程 1 拿了 B 锁请求 A 锁。
5.2 避免死锁的方法
避免死锁要从破坏死锁的必要条件上入手:
- 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
- 打破不可抢占条件:当一线程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
- 打破占有且申请条件:采用资源预先分配策略,即线程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
- 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有线程只能采用按序号递增的形式申请资源。
破坏以上任一条件都可以打破死锁。以上面的代码举例来说,从“打破循环等待条件”这一条出发,可以让两个线程都先争夺同一个锁,而不是一个先争夺 lock1 另一个先争夺 lock2;
从“打破不可抢占条件”这一条出发,可以使用 Lock 锁的“尝试拿锁”机制代替 synchronized 锁:
public class TryLock {private static final Lock lock1 = new ReentrantLock();private static final Lock lock2 = new ReentrantLock();private static class RunnableA implements Runnable {@Overridepublic void run() {String threadName = Thread.currentThread().getName();boolean flagLock2 = false;while (true) {if (lock1.tryLock()) {try {System.out.println(threadName + " got lock1");if (lock2.tryLock()) {flagLock2 = true;try {System.out.println(threadName + " got lock2");System.out.println(threadName + " do work---");break;} finally {lock2.unlock();}}if (!flagLock2) {System.out.println(threadName + " didn't get lock2.Release lock1.");}} finally {lock1.unlock();}}try {Thread.sleep(new Random().nextInt(3));} catch (InterruptedException e) {e.printStackTrace();}}}}private static class RunnableB implements Runnable {@Overridepublic void run() {String threadName = Thread.currentThread().getName();boolean flagLock1 = false;while (true) {if (lock2.tryLock()) {try {System.out.println(threadName + " got lock2");if (lock1.tryLock()) {flagLock1 = true;try {System.out.println(threadName + " got lock1");System.out.println(threadName + " do work---");break;} finally {lock1.unlock();}}if (!flagLock1) {System.out.println(threadName + " didn't get lock1.Release lock2.");}} finally {lock2.unlock();}}try {Thread.sleep(new Random().nextInt(3));} catch (InterruptedException e) {e.printStackTrace();}}}}public static void main(String[] args) {new Thread(new TryLock.RunnableA()).start();new Thread(new TryLock.RunnableB()).start();}
}
Lock 的 tryLock() 返回 true 时表明已经拿到锁,返回 false 表示没有拿到锁。对于两个线程来说,如果只拿到了外层的锁,而没有拿到内层的锁,那么通过在外层的 finally 块中释放外层锁,可以避免死锁。通过死循环与拿到两道锁执行完任务后 break 的搭配可以保证任务一定会被执行一次。一种可能的输出如下:
// Thread-0 先拿到 lock1,但是没拿到 lock2,那么就释放 lock1
Thread-0 got lock1
Thread-0 didn't get lock2.Release lock1
// Thread-1 得到执行权,先后拿到 lock2、lock1
Thread-1 got lock2
Thread-1 got lock1
Thread-1 do work---
// Thread-1 执行完任务将两道锁释放,最后 Thread-0 拿到两道锁执行完任务
Thread-0 got lock1
Thread-0 got lock2
Thread-0 do work---
避免死锁常见的算法有有序资源分配法、银行家算法,这里我们就不详细展开了。
死锁与程序阻塞并不是相同的概念。死锁是所有线程都在等待对方释放锁,而阻塞并不是因为争抢不到同步锁,只是所有线程都处于阻塞状态无法被唤醒去继续执行!
5.3 活锁
上例中 RunnableA 和 RunnableB 的 run() 的最后都使用了 Thread.sleep() 使当前线程进行等待,如果把它们去掉会得到如下的结果:
Thread-0 got lock1
Thread-1 got lock2
Thread-0 didn't get lock2.Release lock1.
Thread-1 didn't get lock1.Release lock2.
Thread-0 got lock1
Thread-1 got lock2
Thread-0 didn't get lock2.Release lock1.
Thread-1 didn't get lock1.Release lock2.
Thread-0 got lock1
Thread-1 got lock2
Thread-0 didn't get lock2.Release lock1.
Thread-1 didn't get lock1.Release lock2.
Thread-0 got lock1
Thread-1 got lock2
Thread-0 didn't get lock2.Release lock1.
Thread-1 didn't get lock1.Release lock2.
…… 省略n多次重复
Thread-0 got lock1
Thread-0 got lock2
Thread-0 do work---
Thread-1 got lock2
Thread-1 got lock1
Thread-1 do work---
可见两个线程拿锁的过程急剧加长了,多次出现了 Thread-0 拿到 lock1,Thread-1 拿到 lock2,然后两个线程拿第二个锁失败的情况,后续又重复了多次这种情况。像这种线程仍处于运行状态,但实际上却没有执行业务代码(一直在做拿锁->释放锁的无用功)的情况,称为活锁。
解决方式就是在锁的范围之外使用 Thread.sleep() 休眠一小段时间,让两个线程拿锁的时间错开一点,进而避免双方各拿到一个锁的局面发生。
Thread.sleep() 会让出 CPU,但是不会释放锁。如果把休眠动作放在锁内,由于本方线程已经拿到的锁并不会释放,即使让出 CPU 时间片给对方线程,对方线程也是拿不到本方已经持有的锁的。所以上边才强调要在锁的范围之外加 Thread.sleep()。
另外,Thread.sleep() 不会释放锁,但是 Object.wait() 会释放锁。调用了前者的线程,时间到了会自动唤醒,而调用了后者的线程会进入线程等待池中等待,只有通过 notify()、notifyAll() 等方法唤醒后,才能重新进入就绪队列抢锁执行。
6、线程安全集合
使用 Collections 工具类可以把线程不安全的 ArrayList、HashSet、HashMap 等集合变成线程安全集合:

除此之外,在 java.util.concurrent 包下有大量支持高效并发访问的集合接口和实现类:

可以分为如下两类:

详细介绍:

7、测试题
1.run 和start的区别 ?
答:run是函数调用 和线程没有任何关系, .start会走底层 会走系统层 最终调度到 run函数,这才是线程。
2.如何控制线程的执行顺序 ?
答:join来控制 让t2获取执行权力,能够做到顺序执行
3.多线程中的并行和并发是什么?
答:四个车道,四辆车并行的走,就是并行, 四个车道中,五秒钟多少的车流量,多少的吞吐量一样
4.在Java中能不能指定CPU去执行某个线程?
答:不能,Java是做不到的,唯一能够去干预的就是C语言调用内核的API去指定才行,这个你回答的话,面试官会觉得你研究点东西
5.在项目开发过程中,你会考虑Java线程优先级吗?
答:不会考虑优先级,为什么呢? 因为线程的优先级很依赖与系统的平台,所以这个优先级无法对号入座,无法做到你想象中的优先级,属于不稳定,有风险
因为某些开源框架,也不可能依靠线程优先级来,设置自己想要的优先级顺序,这个是不可靠的
例如:Java线程优先级又十级,而此时操作系统优先级只有2~3级,那么就对应不上
6.sleep和wait又什么区别?
答:sleep是休眠,等休眠时间一过,才有执行权的资格,注意:只是又有资格了,并不代表马上就会被执行,什么时候又执行起来,取决于操作系统调度
wait是等待,需要人家来唤醒,唤醒后,才有执行权的资格,注意:只是又有资格了,并不代表马上就会被执行,什么时候又执行起来,取决于操作系统调度
含义的不同:sleep无条件可以休眠, wait是某些原因与条件需要等待一下(资源不满足,拿不到同步锁就等待)
7.在Java中能不能强制中断线程的执行?
答:虽然提供了 stop 等函数,但是此函数不推荐使用,为什么因为这种暴力的方式,很危险,例如:下载图片5kb,只下载了4kb 等
我们可以使用interrupt来处理线程的停止,但是注意interrupt只是协作式的方式,并不能绝对保证中断,并不是抢占式的
8.如何让出当前线程的执行权?
答:yield方法,只在JDK某些实现才能看到,是让出执行权
9.sleep,wait,到底那个函数才会 清除中断标记?
答:sleep在抛出异常的时候,捕获异常之前,就已经清除
10.如果错误 错误发生在哪一行?
class Test implements Runnable {public void run(Thread t) {}
}
错误在第一行,应该被 abstract 修饰。其实是 Runnable 接口中的 run() 方法是无参的,而例子中的 run(Thread t) 以 Thread 对象作为参数,其实就是没有实现 Runnable 接口,所以这个类要声明成抽象类。
11.运行结果
new Thread(new Runnable(){public void run(){System.out.println("runnable run");}}){public void run(){System.out.println("subThread run");}}.start();
subThread run。重写了 run 方法就以它为主,没有重写就以 Runnable 接口为主。
相关文章:
线程知识总结(二)
本篇文章以线程同步的相关内容为主。线程的同步机制主要用来解决线程安全问题,主要方式有同步代码块、同步方法等。首先来了解何为线程安全问题。 1、线程安全问题 卖票示例,4 个窗口卖 100 张票: class Ticket implements Runnable {priv…...
解决vscode ssh远程连接服务器一直卡在下载 vscode server问题
目录 方法1:使用科学上网 方法2:手动下载 方法3 在使用vscode使用ssh远程连接服务器时,一直卡在下载"vscode 服务器"阶段,但MobaXterm可以正常连接服务器,大概率是网络问题,解决方法如下: 方…...
【Cadence射频仿真学习笔记】IC设计中电感的分析、建模与绘制(EMX电磁仿真,RFIC-GPT生成无源器件及与cadence的交互)
一、理论讲解 1. 电感设计的两个角度 电感的设计可以从两个角度考虑,一个是外部特性,一个是内部特性。外部特性就是把电感视为一个黑盒子,带有两个端子,如果带有抽头的电感就有三个端子,需要去考虑其电感值、Q值和自…...
Definition of Done
Definition of Done English Version The team agrees on, a checklist of criteria which must be met before a product increment “often a user story” is considered “done”. Failure to meet these criteria at the end of a sprint normally implies that the work …...
【漏洞复现】CVE-2023-37461 Arbitrary File Writing
漏洞信息 NVD - cve-2023-37461 Metersphere is an opensource testing framework. Files uploaded to Metersphere may define a belongType value with a relative path like ../../../../ which may cause metersphere to attempt to overwrite an existing file in the d…...
简单工厂模式和策略模式的异同
文章目录 简单工厂模式和策略模式的异同相同点:不同点:目的:结构: C 代码示例简单工厂模式示例(以创建图形对象为例)策略模式示例(以计算价格折扣策略为例)UML区别 简单工厂模式和策…...
HuggingFace datasets - 下载数据
文章目录 下载数据修改默认保存地址 TRANSFORMERS_CACHE保存到本地 & 本地加载保存加载 读取 .arrow 数据 下载数据 1、Python 代码下载 from datasets import load_dataset imdb load_dataset("imdb") # name参数为full或mini,full表示下载全部数…...
梯度(Gradient)和 雅各比矩阵(Jacobian Matrix)的区别和联系:中英双语
雅各比矩阵与梯度:区别与联系 在数学与机器学习中,梯度(Gradient) 和 雅各比矩阵(Jacobian Matrix) 是两个核心概念。虽然它们都描述了函数的变化率,但应用场景和具体形式有所不同。本文将通过…...
Vscode搭建C语言多文件开发环境
一、文章内容简介 本文介绍了 “Vscode搭建C语言多文件开发环境”需要用到的软件,以及vscode必备插件,最后多文件编译时tasks.json文件和launch.json文件的配置。即目录顺序。由于内容较多,建议大家在阅读时使用电脑阅读,按照目录…...
自定义 C++ 编译器的调用与管理
在 C 项目中,常常需要自动化地管理编译流程,例如使用 MinGW 或 Visual Studio 编译器进行代码的编译和链接。为了方便管理不同编译器和简化编译流程,我们开发了一个 CompilerManager 类,用于抽象编译器的查找、命令生成以及执行。…...
时间管理系统|Java|SSM|JSP|
【技术栈】 1⃣️:架构: B/S、MVC 2⃣️:系统环境:Windowsh/Mac 3⃣️:开发环境:IDEA、JDK1.8、Maven、Mysql5.7 4⃣️:技术栈:Java、Mysql、SSM、Mybatis-Plus、JSP、jquery,html 5⃣️数据库可…...
用SparkSQL和PySpark完成按时间字段顺序将字符串字段中的值组合在一起分组显示
用SparkSQL和PySpark完成以下数据转换。 源数据: userid,page_name,visit_time 1,A,2021-2-1 2,B,2024-1-1 1,C,2020-5-4 2,D,2028-9-1 目的数据: user_id,page_name_path 1,C->A 2,B->D PySpark: from pyspark.sql import SparkSes…...
Sentinel 学习笔记3-责任链与工作流程
本文属于sentinel学习笔记系列。网上看到吴就业老师的专栏,原文地址如下: https://blog.csdn.net/baidu_28523317/category_10400605.html 上一篇梳理了概念与核心类:Sentinel 学习笔记2- 概念与核心类介绍-CSDN博客 补一个点:…...
Latex 转换为 Word(使用GrindEQ )(英文转中文,毕业论文)
效果预览 第一步: 告诉chatgpt: 将latex格式中的英文翻译为中文(符号和公式不要动),给出latex格式第二步: Latex 转换为 Word(使用GrindEQ ) 视频 https://www.bilibili.com/video/BV1f242…...
使用Chat-LangChain模块创建一个与用户交流的机器人
当然!要使用Chat-LangChain模块创建一个与用户交流的机器人,你需要安装并配置一些Python库。以下是一个基本的步骤指南和示例代码,帮助你快速上手。 安装依赖库 首先,你需要安装langchain库,它是一个高级框架&#x…...
国家认可的人工智能从业人员证书如何报考?
一、证书出台背景 为进一步贯彻落实中共中央印发《关于深化人才发展体制机制改革的意见》和国务院印发《关于“十四五”数字经济发展规划》等有关工作的部署要求,深入实施人才强国战略和创新驱动发展战略,加强全国数字化人才队伍建设,持续推…...
【网络云计算】2024第51周-每日【2024/12/17】小测-理论-解析
文章目录 1. 计算机网络有哪些分类2. 计算机网络中协议与标准的区别3. 计算机网络拓扑有哪些结构4. 常用的网络设备有哪些,分属于OSI的哪一层5. IEEE802局域网标准有哪些 【网络云计算】2024第51周-每日【2024/12/17】小测-理论-解析 1. 计算机网络有哪些分类 计算…...
每日十题八股-2024年12月19日
1.Bean注入和xml注入最终得到了相同的效果,它们在底层是怎样做的? 2.Spring给我们提供了很多扩展点,这些有了解吗? 3.MVC分层介绍一下? 4.了解SpringMVC的处理流程吗? 5.Handlermapping 和 handleradapter有…...
网络方案设计
一、网络方案设计目标 企业网络系统的构成 应用软件 计算平台 物理网络及拓扑结构 网络软件及工具软件 网络互连设备 广域网连接 无论是复杂的,还是简单的计算机网络,都包括了以下几个基本元素 : 应用软件----支持用户完成专门操作的软件。…...
学习记录:electron主进程与渲染进程直接的通信示例【开箱即用】
electron主进程与渲染进程直接的通信示例 1. 背景: electronvue实现桌面应用开发 2.异步模式 2.1使用.send 和.on的方式 preload.js中代码示例: const { contextBridge, ipcRenderer} require(electron);// 暴露通信接口 contextBridge.exposeInMa…...
(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...
使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...
QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...
Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...
