线程之间的协作

线程间的共享,防止共享变量冲突,线程安全问题,所以就要有锁机制,synchronized内置锁、volatile、ThreadLocal 用这些解决,如果是线程之前需要协作呢?

线程间协作

有的时候我们需要一个线程修改了一个对象的值之后,另外的线程感知到这个值变化后再进行工作,前一个线程就像是生产者,后面感知变化的像是消费者,那么消费者怎么感知变化呢?

第一种 轮训 就是消费者定时不断轮训这个对象看值是否发生改变,使用轮训的方式有两个缺点:

  • 难以保证及时性
  • 资源开销大

另外一种就是等待和通知机制  生产者修改了对象的值之后主动去发信号量通知(notify)正在等待(wait)这个对象值变化的消费者。

java中使用wait、notify/notifyAll方法实现(注意,这几个线程协作的方法是定义在Object中,都是针对每一个对象而言的),诸如sleep等是Thread线程类的方法,并且wait释放锁,sleep不释锁,要分辩清楚。

实际开发过程中,线程协作部分一般都会遵循一种标准范式

等待和通知的标准范式

等待方:

1、 获取对象的锁;

2、 循环里判断条件是否满足,不满足调用wait方法;

3、 条件满足执行业务逻辑;

通知方:

1、 获取对象的锁;

2、 改变条件

3、 通知所有等待在对象的线程

我们就按这个标准范式完成一个线程协作的示例:

/**
 * 类说明:快递类   定义这个快递走了多少公里和到达了哪个站点
 * 并且定义了改变公里/到达后通知方法  以及等待公里/到点变化方法
 */
public class Express {
    public final static String CITY = "ShangHai";
    private int km;/*快递运输里程数*/
    private String site;/*快递到达地点*/

    public Express() {
    }

    public Express(int km, String site) {
        this.km = km;
        this.site = site;
    }

    /* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
    public synchronized void changeKm() {
        this.km = 101;
        notifyAll();
        //其他的业务代码
    }

    /* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
    public synchronized void changeSite() {
        this.site = "BeiJing";
        notifyAll();
    }

    /*等待公里数变化,满足条件后再做其他业务代码*/
    public synchronized void waitKm() {
        while (this.km <= 100) {
            try {
                System.out.println("check Km thread[" + Thread.currentThread().getId()
                        + "] is will wait.");
                wait();
                //执行到下面这一句 说明等待被唤醒
                System.out.println("check km thread[" + Thread.currentThread().getId()
                        + "] is be notified.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //业务操作
        System.out.println("the km is" + this.km + ",I will  XXX...");
    }
    
    /*等待到达点变化,满足条件后再做其他业务代码*/
    public synchronized void waitSite() {
        while (CITY.equals(this.site)) {
            try {
                System.out.println("check site thread[" + Thread.currentThread().getId()
                        + "] is will wait.");
                wait();
                //执行到下面这一句 说明等待被唤醒
                System.out.println("check site thread[" + Thread.currentThread().getId()
                        + "] is be notified.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //业务操作
        System.out.println("the site is" + this.site + ",I will XXX...");
    }

执行 waitKm()方法的线程等待里程数大于100后再执行其他操作,执行changeKm()方法的线程改变里程数大于100后在通知处于等待里程数变化的线程继续执行,改变到达点和等待到达点的方法类似。等待唤醒前要获取锁,所以四个主要方法都加了synchronized的关键字。

测试类:

public class TestWN {
    private static Express express = new Express(0, Express.CITY);

    /*检查里程数变化的线程,不满足条件,线程一直等待*/
    private static class CheckKm extends Thread {
        @Override
        public void run() {
            express.waitKm();
        }
    }

    /*检查地点变化的线程,不满足条件,线程一直等待*/
    private static class CheckSite extends Thread {
        @Override
        public void run() {
            express.waitSite();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {//三个线程 监听 城市的变化
            new CheckSite().start();
        }
        for (int i = 0; i < 3; i++) {//监听里程数的变化
            new CheckKm().start();
        }

        Thread.sleep(1000);//此处睡眠1秒 保证上面线程都已经启动起来
        express.changeKm();//快递里程变化   线程启动后都处于等待状态
    }

}

分别启动了三个监听里程或到达点城市变化的线程,线程启动后做的事就是等待同一个快递里程或城市变化,29行 我们改变了这个快递的公里数至101

执行结果: 

check site thread[12] is will wait.
check site thread[13] is will wait.
check site thread[14] is will wait.
check Km thread[16] is will wait.
check Km thread[15] is will wait.
check Km thread[17] is will wait.
check km thread[17] is be notified.
the km is101,I will  XXX...
check km thread[15] is be notified.
the km is101,I will  XXX...
check km thread[16] is be notified.
the km is101,I will  XXX...
check site thread[14] is be notified.
check site thread[14] is will wait.
check site thread[13] is be notified.
check site thread[13] is will wait.
check site thread[12] is be notified.
check site thread[12] is will wait.

1-6行,是6个线程启动处于等待公里/城市变化,我们改变了公里数后在changeKm里notifyAll了所有处于这个对象等待的6个线程(7-18行),只有等待Km的线程满足了公里数大于100的业务条件后执行业务代码(8、10、12),而等待城市变化方法被唤醒后发现城市还是

shanghai线程又继续等待(14、16、18)。

用notify还是用notifyAll?

上例中我们唤醒时用的方法是notityAll而不是notity,假如我们把changeKm中唤醒方法改为notify,6个等待的线程只有一个接收到信号量会被唤醒,其他5个线程因为信号量丢失不会被唤醒。所以除非明确知道只有一个线程处于等待状态,其他都建议用notifyAll方法,避免有线程被长时间等待下去甚至永远等待下去。所以为了防止信号量丢失,尽量使用notifyAll。

等待超时模式

 其实上面的示例是有漏洞的,我们可以想一下,如果线程进入等待后永远没被唤醒或唤醒后还是满足不了业务条件再次进入等待状态,进入等待后线程什么时候能唤醒就变的不可控甚至永远卡在这了,所以一般我们会引入等待超时模式,超时等待后进行自唤醒,正如我们熟知的连接池取连接

有超时时间不能永远等待下去一样。

等待超时模式伪代码如下:

//假设  等待超时间时长为T   当前时间now + T 时刻后超时
long  overtime = now+T;
long  remain = T;//初始化为T  等待的剩余时间

while(result不满足条件&&remain>0){//如果remain<0了还没有满足条件 也不继续进入等待了
      wait(remain);//这里我们不再直接wait(),而是指明等待多长时间
      remain = overtime – now;//因为上一步的wait有可能是被其他线程提前唤醒的  等待时间没被永远 这里更新剩下的持续时间
}

return result;

使用等待超时模式模拟数据库连接池

我们接下来就使用等待超时模式自己模拟实现一个数据库连接池。

首先实现Connection接口模拟一个数据库连接的实现,并定义fetchConnection方法创建一个数据库连接。(连接实现不是本示例重点,这里简单模拟)

/**
 *
 *类说明:模拟数据库连接的实现
 */
public class SqlConnectImpl implements Connection{
   
    /*拿一个数据库连接*/
    public static final Connection fetchConnection(){
        return new SqlConnectImpl();
    }
   ......
}

 然后我们实现自定义连接池DBPool类,主要实现用等待超时模式拿取连接的fetchConn方法,另外定义用完后放回池连接的releaseConn方法。

/**
 *类说明:实现一个数据库的连接池
 */
public class DBPool {
    
    //数据库池的容器
    private static LinkedList<Connection> pool = new LinkedList<>();
    //实例化池时初始化好初始化数量的连接并放入池中
    public DBPool(int initalSize) {
        if(initalSize>0) {
            for(int i=0;i<initalSize;i++) {
                pool.addLast(SqlConnectImpl.fetchConnection());
            }
        }
    }
    
    //拿连接的方法,等待超时模式 在mills时间内还拿不到数据库连接,返回一个null,放弃取连接
    public Connection fetchConn(long mills) throws InterruptedException {
        synchronized (pool) {
            //如果拿连接时传入的时间参数<0 就表示永不超时
            if (mills<0) {
                while(pool.isEmpty()) {
                    pool.wait();
                }
                return pool.removeFirst();
            }else {
                //等待超时模式的核心代码
                long overtime = System.currentTimeMillis()+mills;
                long remain = mills;
                while(pool.isEmpty()&&remain>0) {
                    pool.wait(remain);
                    remain = overtime - System.currentTimeMillis();
                }
                Connection result  = null;
                if(!pool.isEmpty()) {
                    result = pool.removeFirst();
                }
                return result;
            }
        }
    }
    
    //放回数据库连接的方法
    public void releaseConn(Connection conn) {
        if(conn!=null) {
            synchronized (pool) {
                pool.addLast(conn);
                //有线程释放了连接就唤醒等待其他等待取连接的线程
                pool.notifyAll();
            }
        }
    }
}

27-38行是拿取连接时等待超时时间的核心代码,也就是上面伪代码的实现。49行在放回数据库连接时最后就调用一下notifyAll唤醒等待拿连接的线程。

我们写代码进行测试,场景是用50个线程取连接池里的连接,每个线程尝试取20次进行操作,也就是总共会尝试取1000次,最后统计有多少次取成功了,又有多少次超时等待之后也没有拿到连接。

/**
 *
 *类说明:连接池的测试类
 */
public class DBPoolTest {
    static DBPool pool  = new DBPool(10);
    // 控制器:控制main线程将会等待所有Woker结束后才能继续执行
    static CountDownLatch end;

    public static void main(String[] args) throws Exception {
        // 线程数量
        int threadCount = 50;
        end = new CountDownLatch(threadCount);
        int count = 20;//每个线程的尝试拿连接的操作数据库次数
        AtomicInteger got = new AtomicInteger();//计数器:统计拿到连接的次数
        AtomicInteger notGot = new AtomicInteger();//计数器:统计没有拿到连接的次数
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new Worker(count, got, notGot), 
                    "worker_"+i);
            thread.start();
        }
        end.await();// main线程在此处等待 所有线程的操作完成后main线程再进行下面的统计操作
        System.out.println("总共尝试了: " + (threadCount * count));
        System.out.println("拿到连接的次数:  " + got);
        System.out.println("没能连接的次数: " + notGot);
    }

    static class Worker implements Runnable {
        int           count;
        AtomicInteger got;
        AtomicInteger notGot;

        public Worker(int count, AtomicInteger got,
                               AtomicInteger notGot) {
            this.count = count;
            this.got = got;
            this.notGot = notGot;
        }

        public void run() {
            while (count > 0) {
                try {
                    // 从线程池中获取连接,如果1000ms内无法获取到,将会返回null
                    // 分别统计连接获取的数量got和未获取到的数量notGot
                    Connection connection = pool.fetchConn(1000);
                    if (connection != null) {
                        try {
                            //模拟数据库业务操作
                            connection.createStatement();
                            connection.commit();
                        } finally {
                            pool.releaseConn(connection);
                            got.incrementAndGet();
                        }
                    } else {
                        notGot.incrementAndGet();
                        System.out.println(Thread.currentThread().getName()
                                +"获取连接等待超时!");
                    }
                } catch (Exception ex) {
                } finally {
                    count--;
                }
            }
            //这个线程的20次操作都完成后 计数器减一
            end.countDown();
        }
    }

总共有125次获取连接等待超时没有拿到连接。这就是等待超时时间的应用,防止无限期等待下去。

Join方法   "插队" 

有的时候我们需要在一个线程执行过程中让其它的线程“插队”执行,它执行完之后再执行自己的后面的工作,这时候我们就可以用到join方法。(面试点)

Join:线程A,执行了线程Bjoin方法,线程A必须要等待B执行完成了以后,线程A才能继续自己的工作。

 当然被join进来的线程也可以join其它的线程进来。就像你让别人插队到你前面,那这个人也有可能让他的室友插队到他的前面,那你什么时候能打到饭就不一定喽...

 我们用一个示例演示一下join的使用吧

 /**
  *
  *类说明:演示下join方法的使用
  */
 public class UseJoin {
     
     static class JumpQueue implements Runnable {
         private Thread thread;//用来插队的线程
 
         public JumpQueue(Thread thread) {
             this.thread = thread;
         }
 
         public void run() {
             try {
                 System.out.println(thread.getName()+" will be join before "
                         +Thread.currentThread().getName());
                 thread.join();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName()+" terminted.");
         }
     }
 
     public static void main(String[] args) throws Exception {
         Thread previous = Thread.currentThread();//将要插队的线程 初始化 现在是主线程
         for (int i = 0; i < 10; i++) {
             Thread thread = new Thread(new JumpQueue(previous),String.valueOf(i));
             thread.start();
             //这里休眠一下只是想让打出来的插队日志顺序有规律 因为start方法只是就绪状态了 cpu不一定会运行它 休眠一下等待几乎确认执行到了到再去start就绪下一个线程打插队日志
             SleepTools.second(1);
             //将要插队的线程更新为当前线程
             previous=thread;
         }
 
         SleepTools.second(2);//让主线程休眠2秒
         System.out.println(Thread.currentThread().getName() + " terminate.");
     }
 }

每一个JumpQueue都有一个想被插队到它前面的线程,启动了10线程做JumpQueue。

打印结果:

main will be join before 0
0 will be join before 1
1 will be join before 2
2 will be join before 3
3 will be join before 4
4 will be join before 5
5 will be join before 6
6 will be join before 7
7 will be join before 8
8 will be join before 9
main terminate.
0 terminted.
1 terminted.
2 terminted.
3 terminted.
4 terminted.
5 terminted.
6 terminted.
7 terminted.
8 terminted.
9 terminted.

Process finished with exit code 0

main,0,1,2...线程 前面线程依次插队到后面线程前,一个等待一个。9等待8执行完,8等待7执行完...只有最前面的main执行完,0才执行完,1才执行完,2........

总结 调用yield() 、sleep()、wait()、notify()等方法对锁的影响

我们最后总结一下几个主要的线程共享和协作基础方法对锁的影响。

yield和sleep 都是线程的方法,Thread.yield(),Thread.sleep() 都交出cpu执行权但都不释放锁,区别是yield方法交出cpu后会进入可运行状态(就绪状态)继续争夺cpu执行权,而sleep在休眠过程中进入的是阻塞状态,休眠结束之后才会进入就绪状态。

wait,notify/nofifyAll方法,Object的方法,执行之前都要获取到锁wait方法后会释放锁交出cpu执行权进入阻塞状态,

notify不会主动释放锁,只有锁的代码块执行完才会释放锁(因为在持有锁的代码块里调用了notify后面如果还有业务代码怎么办,得继续执行啊不可能立即丢掉锁,但一般为了能唤醒之后让等待的线程更好的获取锁,都是在锁代码块最后一行再调用notify唤醒)

所以释放锁的方法是wait。    notify、yield、sleep都不释放锁

posted @ 2021-12-31 09:14  PencyNoBug  阅读(116)  评论(0)    收藏  举报