文章目录
前言
java线程可以分为设计与执行,设计无非就是调用start()并且等待JVM为我们映射到一个操作系统线程,并执行它的run()方法,而设计层面就是布置任务。
我们一般将“任务代码”抽取出一个资源类,而不是直接写在线程类的run方法。(synchronized尽量不出现在run方法中)
class res{
static int i;
int j;
synchronized void a(){}
synchronized void b(){}
}
如果我们将资源访问代码放到一个资源类,那么我们可以创建一个资源实例,然后创建多个线程对象同步地执行该任务。
Task3_ task = new Task3_(); //资源类
new Thread(task::first).start(); //线程操作资源类
new Thread(task::second).start();
new Thread(task::third).start();
如果将syn(this)或者syn方法声明在资源类中,对象锁指代的就是内存中存在的task对象。而如果是类锁,指代的是加载入内存的唯一的Task_的Class对象。
而如果将syn关键字申明在run方法中,对象锁指代的是的“runnable”对象绑定的锁对象,没有意义。推荐使用外部传入synchronize包裹的锁对象。
同一时刻,系统中只有某一个线程正在执行同步块,而如果看向res的话,就意味着a和b方法只有一个正在被调用,请求执行另一个方法的线程被阻塞,因为未抢到对象锁。(底层是CAS修改锁对象owner失败,阻塞进锁关联的同步队列)。
但是如果a方法是syn修饰而b方法用syn static修饰就不会影响,因为此时不是同一把锁了,如果有两个对象new res()分别为res1和res2,也不影响,因为两个对象分别代表不同的锁。
总结:
线程的任务代码应该单独存放在资源类中,目的是明确锁对象和实现高内聚低耦合。资源类中必不可少的代码:成员字段和操作成员字段的synchronized修饰的代码。因为同步问题解决的问题就是实现互斥地访问临界资源,所以必须能够抽象出“资源值”字段。
实现生产者和消费者通信模型
生产者和消费者模型就是一个典型的多线程操作资源类的模型,进一步简化,共享资源就是一个数字,一组线程负责增,一组线程负责减,当数字为0就不能继续减,而当数字达到一个上限则不能继续增
生产者与消费者模型的高层次实现就是使用阻塞队列。因此实现生产者消费者模型其实也是实现阻塞队列的思路。
依据高内聚和低耦合原则,操作资源类的代码应该定义在资源类中,而用户(线程对象)仅仅调用资源类对外暴露的接口即可。
class P implements Runnable{
private Resource target ;
public P(Resource target) {
this.target = target;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
target.produce();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过构造函数传入资源实例,任务:生产十次。消费者代码逻辑十分相似
class C implements Runnable{
private Resource target ;
public C(Resource target) {
this.target = target;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
target.consume();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类中,创建一个公共的资源类,以及两对生产者消费者。
Resource r = new Resource(5);//指定一个上限
new Thread(new P(r),"生产者A").start();
new Thread(new P(r),"生产者B").start();
new Thread(new C(r),"消费者A").start();
new Thread(new C(r),"消费者B").start();
synchronized与wait/notify
class Resource{
private int num;
private int maxSize;
public Resource(int maxSize) {
this.maxSize = maxSize;
}
public synchronized void produce() throws InterruptedException {
while (num==maxSize){
wait(); // 这里的锁对象是当前resource实例绑定的monitor
}
System.out.println(Thread.currentThread().getName()+"生产后:"+num);
num++;
System.out.println(Thread.currentThread().getName()+"生产前:"+num);
notifyAll();
}
public synchronized void consume() throws InterruptedException {
while (num==0){
wait();
}
System.out.println(Thread.currentThread().getName()+"消费前"+num);
num--;
System.out.println(Thread.currentThread().getName()+"消费后"+num);
notifyAll();
}
}
notifyAll()属于粗唤醒,一个消费者消费完毕,不但会唤醒生产者线程,还会唤醒某些消费者线程,这些被“虚假唤醒”的消费者线程醒来后,发现不满足条件将会调用wait继续睡眠,而这涉及一次线程切换(wait调用涉及系统调用),而且被虚假唤醒的线程拿到CPU什么有意义的事情也没干却成功抢占了CPU,这是低效的
reentrantLock与condition
class Resource2{
private int num;
private int maxSize;
private ReentrantLock lock=new ReentrantLock();
private Condition empty = lock.newCondition();
private Condition full = lock.newCondition();
public Resource2(int maxSize) {
this.maxSize = maxSize;
}
public void produce() throws InterruptedException {
lock.lock();
try {
while (num==maxSize){
full.await();
}
num++;
empty.await();
}finally {
lock.unlock();
}
}
public synchronized void consume() throws InterruptedException {
lock.lock();
try {
while (num==0){
empty.await();
}
num--;
full.await();
}finally {
lock.unlock();
}
}
}
基于reentrantLock的一个好处就是可以使用细粒度等待队列,减少不必要的虚假唤醒情况。
总结:消费者与生产者模型的实现和实现阻塞队列一个道理,主要就是需要处理好等待与唤醒的时机。
交替打印问题
建立三个线程ABC,各自分别打印10次A 、B和 C
实现交替打印,按照ABCABC…
synchronized实现
private char cur = 'A';
private int sum = 30; //打印十轮为例
//任务打印字母,并且修改字母
void doTask(char name){
synchronized (this){
while (sum>0){
if(cur == name){
sum--;
System.out.println(Thread.currentThread().getName()+" 当前正在打印 "+name+" sum:"+sum);
//更新字母
if(name=='A')cur='B';
else if(name=='B')cur='C';
else cur='A';
}else {
//唤醒另外两个字母进程
this.notifyAll();
//等到cur==name再执行任务
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
this.notifyAll();//任务完成
}
}
synchronized可以保证可见性,所以没有使用volatile修饰共享变量。
这道题中,前一个被打印的字母和打印次数,就是需要同步的资源。由于线程之间有序,因此离不开通信机制wait/notify。
假设某一时刻锁被B线程抢占,而此时的同步字母为A,说明这是一次无效的抢占,于是B线程notifyAll唤醒其他两个线程,然后this.wait()进入对象锁对应的阻塞队列。
直到线程A抢到了锁,成为了锁的owner。
A进程第一次执行if中的逻辑,修改字段值为B和sum–,然后执行了一次else,唤醒全部的wait进程(主要是唤醒C线程),然后进入waitSet阻塞。
(这种写法属于粗唤醒,会产生大量无效的唤醒,效率比较低)
JUC实现精确唤醒
ReentrantLock lock =new ReentrantLock();
Condition condition1 =lock.newCondition();
Condition condition2 =lock.newCondition();
Condition condition3 =lock.newCondition();
我们创建三个条件对象,分别用于三个线程,等待的原因(condition)可以分为:当前不是A、当前不是B和当前不是C
这里不再抽取出一个方法,而是定义三个方法,当前只展示A线程调用的方法first(),另外两个无非就是修改一下字母
void first(){
//锁写在循环外,避免不必要竞争,只获取一次即可
lock.lock();
try {
for (int i = 0; i < 10; i++) {
while (syn != 'A') {//写在循环内,防止虚假唤醒
condition1.await();//非当前线程的任务,wait
}
System.out.println(Thread.currentThread().getName() + " " + i + " A");
syn = 'B';
//执行完任务后,精确唤醒下一个线程,同时wait
condition2.signal();//只唤醒B线程(精确唤醒)
condition1.await();
}
condition2.signal();//最后一次唤醒。(最后一次比较特殊,避免死锁)
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
信号量实现
Semaphore(1)等价于互斥锁,而semaphore(0)可以用于实现前驱关系
private Semaphore s1 = new Semaphore(0);
private Semaphore s2 = new Semaphore(0);
private Semaphore s3 = new Semaphore(0);
private volatile int sum;
void first(){
while (true){
try {
s1.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" == "+ ++sum);
s2.release();
if(sum==28)return;
}
}
void second(){
while (true){
try {
s2.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" == "+ ++sum);
s3.release();
if(sum==29)return;
}
}
当A B线程会阻塞,而C线程会先唤醒A,然后阻塞,接着A唤醒B,B唤醒C依次类推。这里仅展示其中一个线程执行的方法
void third(){
while (true){
s1.release();
try {
s3.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" == "+ ++sum);
System.out.println("================================================");
if(sum==30)return;
}
}
自旋读写volatile实现
三个线程并发执行,cur为A B C中任何一个,由于使用了volatile关键字保证内存可见性,某一个时刻cur和num被修改,其他线程将立刻知道
void first(){ // cur 和 num 都是共享变量,且由volatile修饰
while (num<28){
while (cur!='C');//自旋
++num;
System.out.println(Thread.currentThread().getName()+ " == A " + num);
cur='A';
}
}
park/unpark实现
park/unpark都是以线程对象作为单位进行调用的。lockSupport不需要获取对象的monitor,而是给线程一个许可。
Permit的值只能是0和1(二元变量),unpark会给线程一个permit,最多是一个(这里可以理解为置位),而park()会消耗一个permit并返回,如果线程没有permit将被阻塞(试图重置为0,如果当前即为0则阻塞)
private static Thread A,B,C;
public static void main1(String[] args) {
A = new Thread(()->{
for (int i = 0; i < 10; i++) {
//第一次调用park(),A进程会被阻塞
LockSupport.park();
System.out.println(Thread.currentThread().getName()+" 当前正在打印 A"+" 次数:"+i);
LockSupport.unpark(B);//唤醒B进程
}
});
B = new Thread(()->{
for (int i = 0; i < 10; i++) {
LockSupport.park();//B进程阻塞
System.out.println(Thread.currentThread().getName()+" 当前正在打印 B"+" 次数:"+i);
LockSupport.unpark(C);//唤醒C进程
}
});
C = new Thread(()->{
for (int i = 0; i < 10; i++) {
LockSupport.unpark(A);//唤醒A进程
LockSupport.park();//阻塞
System.out.println(Thread.currentThread().getName()+" 当前正在打印 C"+" 次数:"+i);
}
});
A.start();B.start();C.start();
}
由于每个线程对象的permit初始都为0,因此上面的原理其实类型semaphore(0)实现前驱关系。