首页 > 编程语言 > 详细

多线程交互

时间:2021-05-04 17:27:13      阅读:20      评论:0      收藏:0      [点我收藏+]

多线程交互

synchronized同步

synchronized同步对象概念

Object someObject =new Object();
synchronized (someObject){
	//此处的代码只有占有了someObject后才可以执行
}

synchronized表示当前线程独占对象 someObject,如果有其他线程试图占有对象someObject,就会等待,直到当前线程释放对someObject的占用。someObject又叫同步对象,或者称为对象监视器,所有的对象,都可以作为同步对象,为了达到同步的效果,必须使用同一个同步对象。

释放同步对象的方式: synchronized块自然结束,或者有异常抛出。

对于synchronized修饰的对象或方法,为了实现多线程交互,需要使用几个方法:wait(), notify(),notifyAll()

wait()方法和notify()方法,并不是Thread线程上的方法,它们是Object上的方法。因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait()notify()是同步对象上的方法。

  • wait()的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait()是有前提条件的,一定是在synchronized块里,否则就会出错。

  • notify() 的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。

  • notifyAll() 的意思是,通知所有等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。

example1:请用三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC....”的字符串。

package multiplethread;

public class PrintABCUsingWaitNotify {
    private int state;
    private int times;
    private static final Object monitor = new Object();

    public PrintABCUsingWaitNotify(int times){
        this.times = times;
    }

    public void printLetter(String name, int targetState){
        for(int i=0; i<times; i++){
            synchronized (monitor){
                while (state%3!=targetState){
                    try {
                        monitor.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
                state++;
                System.out.print(name);
                monitor.notifyAll();
            }
        }
    }
    public static void main(String[] args) {
        PrintABCUsingWaitNotify printABCUsingWaitNotify = new PrintABCUsingWaitNotify(10);

        new Thread(()->{printABCUsingWaitNotify.printLetter("A", 0); }, "A").start();
        new Thread(()->{printABCUsingWaitNotify.printLetter("B", 1);}, "B").start();
        new Thread(()->{printABCUsingWaitNotify.printLetter("C", 2);}, "C").start();
    }
}

example2: 用两个线程交替打印0~10的整数,一个线程打印奇数,一个线程打印偶数

package multiplethread;

public class EvenOddPrinter {
    private int limit;
    private volatile int count;
    private static final Object monitor = new Object();

    public EvenOddPrinter(int count, int limit){
        this.count = count;
        this.limit = limit;
    }

    public void print0(){
        synchronized (monitor){
            while(count<=limit){
                try {
                    System.out.println(String.format("线程[%s]正在打印%d", Thread.currentThread().getName(), count++));
                    monitor.notifyAll();
                    monitor.wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            monitor.notifyAll();
        }
    }

    public static void main(String[] args) {
        EvenOddPrinter printer = new EvenOddPrinter(0, 10);
        new Thread(()->{printer.print0();}, "even").start();
        new Thread(()->{printer.print0();}, "odd").start();
    }
}

example3:用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z

package multiplethread;

public class DigitLetterPrinter {
    private static final Object monitor = new Object();
    private volatile int i = 0;

    private void print0(){
        synchronized (monitor){
            for(int i=0; i<26; i++){
                if(Thread.currentThread().getName().equals("Digit")){
                    System.out.print(i+1);
                    monitor.notifyAll();
                    try{
                        monitor.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }else if(Thread.currentThread().getName().equals("Letter")){
                    System.out.print((char) (i+‘A‘));
                    monitor.notifyAll();
                    try {
                        monitor.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
            monitor.notifyAll();
        }
    }

    public static void main(String[] args) {
        DigitLetterPrinter printer = new DigitLetterPrinter();
        new Thread(()->{printer.print0();}, "Digit").start();
        new Thread(()->{printer.print0();}, "Letter").start();
    }
}

Lock对象

Lock是一个接口,为了使用一个Lock对象,需要用到

Lock lock = new ReentrantLock(); 

synchronized (someObject)类似的,lock()方法,表示当前线程占用lock对象,一旦占用,其他线程就不能占用了。与 synchronized 不同的是,一旦synchronized 块结束,就会自动释放对someObject的占用。Lock却必须调用unlock()方法进行手动释放,为了保证释放的执行,往往会把unlock() 放在finally中进行。

还是用lock完成上面的example1:请用三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC....”的字符串。

package multiplethread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class PrintABCUsingLock {
    private int times;
    private int state;
    //private volatile int i;
    private Lock lock = new ReentrantLock();

    public PrintABCUsingLock(int times){
        this.times = times;
    }

    private void print0(String name, int targetNum){
        for(int i=0; i<times;){
            lock.lock();
            if(state%3==targetNum){
                state++;
                i++;
                //System.out.println("state:" + state + " i: " +i);
                System.out.print(name);
            }
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        PrintABCUsingLock printer = new PrintABCUsingLock(10);
        new Thread(()->{printer.print0("B", 1);}, "B").start();
        new Thread(()->{printer.print0("C", 2);}, "C").start();
        new Thread(()->{printer.print0("A", 0);}, "A").start();
    }
}

main 方法启动后,3 个线程会抢锁,但是 state 的初始值为 0,所以第一次执行 if 语句的内容只能是 线程 A,然后还在 for 循环之内,此时 state = 1,只有 线程 B 才满足 1% 3 == 1,所以第二个执行的是 B,同理只有 线程 C 才满足 2% 3 == 2,所以第三个执行的是 C,执行完 ABC 之后,才去执行第二次 for 循环,所以要把 i++ 写在 for 循环里边,不能写成 for (int i = 0; i < times;i++) 这样。这里我还是想不明白,state++和i++为什么不会同时执行?

Lock/Condition

使用synchronized方式进行线程交互,用到的是同步对象的wait(),notify()notifyAll()方法,

Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await(), signal(),signalAll() 方法.

注意: Object 中的 wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而 Condition 是需要与"互斥锁"/"共享锁"捆绑使用的。

example4:多线程按顺序调用,A->B->C,A打印 3 次,BB 打印2 次,CC 打印4次,重复 10 次

package multiplethread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class PrintABCUsingLockCondition {
    private int state;
    private int times;
    private static Lock lock = new ReentrantLock();
    private static Condition c1 = lock.newCondition();
    private static Condition c2 = lock.newCondition();
    private static Condition c3 = lock.newCondition();

    public PrintABCUsingLockCondition(int times){
        this.times = times;
    }

    public void print0(String name, int targetState, Condition cur, Condition next, int count){
        for(int i=0; i<times;){
            lock.lock();
            try {
                while(state%3!=targetState){
                    cur.await();
                }
                for(int k=0; k<count; k++) System.out.print(name);
                state++;
                i++;
                next.signal();
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        PrintABCUsingLockCondition printer = new PrintABCUsingLockCondition(10);
        new Thread(()->{printer.print0("A", 0, c1, c2, 3);}, "A").start();
        new Thread(()->{printer.print0("B", 1, c2, c3, 2);}, "B").start();
        new Thread(()->{printer.print0("C", 2, c3, c1, 4);}, "C").start();
    }
}

trylock()

synchronized 不占用到锁是不罢休的,会一直试图占用下去。与 synchronized 的钻牛角尖不一样,Lock接口还提供了一个trylock()方法。trylock()会在指定时间范围内试图占用,占成功了,执行相关的业务代码。 如果时间到了,还占用不成功,会放弃占用。

注意: 因为使用trylock()有可能成功,有可能失败,所以后面unlock()释放锁的时候,需要判断是否占用成功了,如果没占用成功也unlock(),就会抛出异常。

package multiplethread;
 
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class TestThread { 
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
 
        Thread t1 = new Thread() {
            public void run() {
                boolean locked = false;
                try {
                    locked = lock.tryLock(1,TimeUnit.SECONDS);
                    if(locked){
                        // do something
                        Thread.sleep(5000);
                    }
                    else{
                        System.out.println("exit!");
                    }
 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {                     
                    if(locked){
                        lock.unlock();
                    }
                }
            }
        };
        t1.setName("t1");
        t1.start();
        
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        
        Thread t2 = new Thread() { 
            public void run() {
                boolean locked = false;
                try {
                    locked = lock.tryLock(1,TimeUnit.SECONDS);
                    if(locked){
                        // do something
                        Thread.sleep(5000);
                    }
                    else{
                        System.out.println("exit!");
                    }
 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {                     
                    if(locked){
                        lock.unlock();
                    }
                }
            }
        };
        t2.setName("t2");
        t2.start();
    }
 
}

lock和synchronized的区别

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。

  2. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。

  3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。

Semaphore

发音[‘sem?f?:]

Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。

Semaphore的主要方法:

  void acquire():从此信号量获取一个许可(获取成功,信号量减一),在提供一个许可前一直将线程阻塞,直到有线程释放(release)信号,或者超时。

  void release():释放一个许可,将其返回给信号量(释放成功,信号量加一)。

  int availablePermits():返回此信号量中当前可用的许可数。

  boolean hasQueuedThreads():查询是否有线程正在等待获取。

用semaphore解决上面的example3:用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z

package multiplethread;

import java.util.concurrent.Semaphore;

public class PrintDigitLetterUsingSemaphore {
    private static Semaphore digitSemaphore = new Semaphore(1);
    private static Semaphore letterSemaphore = new Semaphore(0);

    public void print0(Semaphore cur, Semaphore next){
        for(int i=0; i<26; i++){
            try{
                cur.acquire();
                if(Thread.currentThread().getName().equals("Digit")){
                    System.out.print(i+1);
                }else if(Thread.currentThread().getName().equals("Letter")){
                    System.out.print((char)(i+‘A‘));
                }
                next.release();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        PrintDigitLetterUsingSemaphore printer = new PrintDigitLetterUsingSemaphore();
        new Thread(()->{printer.print0( digitSemaphore, letterSemaphore);}, "Digit").start();
        new Thread(()->{printer.print0( letterSemaphore, digitSemaphore);}, "Letter").start();
    }
}

example4:通过 N 个线程顺序循环打印从 0 至 20

package multiplethread;

import java.util.concurrent.Semaphore;

public class PrintMultiThreadInOrder {
    private static final int THREAD_COUNT = 5;
    private static int maxNum = 20;
    private static int num = 0;


    public static void main(String[] args) throws InterruptedException {
        final Semaphore[] semaphores = new Semaphore[THREAD_COUNT];
        for(int i=0; i<THREAD_COUNT; i++){
            semaphores[i] = new Semaphore(1);
            // 除了最后一个信号量,其他的信号量都为0,也就是最后一个syncObject不会被阻塞
            if(i!=THREAD_COUNT-1) {
                semaphores[i].acquire();
            }
        }

        for(int i=0; i<THREAD_COUNT; i++){
            // 信号量数组每个信号量都有位于其前面的信号量,第0个信号量的前一个信号量为数组最后一个信号量
            // 通过两个信号量的acquire()和release()操作来保证按顺序打印
            Semaphore lastSemaphore = i==0 ? semaphores[THREAD_COUNT-1] : semaphores[i-1];
            Semaphore curSemaphore = semaphores[i];
            final int idx = i;
            new Thread(()->{
                try{
                    while (true){
                        // 初次执行,让第一个 for 循环没有阻塞的 syncObjects[4] 先获得令牌阻塞了
                        lastSemaphore.acquire();
                        System.out.println("Thread" + idx + ": " + num++);
                        if(num>maxNum){
                            System.exit(0);
                        }
                        // 释放当前信号量,刚好是下个for循环的lastSemaphore,从而保证是按数组信号量的顺序依次打印
                        curSemaphore.release();
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

LockSupport

LockSupprot是线程的阻塞原语,用来阻塞线程和唤醒线程。每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在线程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark() 使其可用。但是注意许可不可重入,也就是说只能调用一次park()方法,否则会一直阻塞。(在 AQS 中,就是通过调用 LockSupport.park( )LockSupport.unpark() 来实现线程的阻塞和唤醒的。)

这里用LockSupport实现example5: 用N个线程交替打印0~M的整数。

package multiplethread;

import java.util.concurrent.locks.LockSupport;

public class PrintDigitUsingLockSupport {

    private static final int THREAD_COUNT = 5;
    private static int maxNum = 20;
    private static int cur = 0;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREAD_COUNT];
        Printer[] printers = new Printer[THREAD_COUNT];
        for(int i=0; i<THREAD_COUNT; i++){
            printers[i] = new Printer();
            threads[i] = new Thread(printers[i], String.valueOf(i+1));
        }
        for(int i=0; i<THREAD_COUNT; i++){
            Thread nextThread = threads[i==THREAD_COUNT-1 ? 0 : i+1];
            printers[i].setNextThread(nextThread);
            threads[i].start();
        }
        //for(int i=0; i<THREAD_COUNT; i++) threads[i].start();
        LockSupport.unpark(threads[0]);
    }
    // 这里用了一个辅助类Printer来实现run()方法,同时给它增加了一个setNextThread的方法,方便在
    // 实例化下一个Thread之后再进行setNextThread操作,这样可以保证run方法中的nextThread是我们
    // 想要的Thread,而不是空的
    static class Printer implements Runnable{
        private volatile Thread nextThread;
        @Override
        public void run() {
            while (true){
                // 所有线程一开始都是i处于阻塞等待唤醒状态
                LockSupport.park();
                System.out.println("Thread" + Thread.currentThread().getName() + ": " + cur++);
                if(cur>maxNum) System.exit(0);
                // 唤醒下一个线程
                LockSupport.unpark(nextThread);
            }
        }

        public void setNextThread(Thread nextThread){
            this.nextThread = nextThread;
        }
    }
}

接下来用LockSupport来解决上面的example3:用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z,这里相对来说比较简洁,因为只有两个线程,可以直接把线程的实例放到LockSupportunpack()中进行唤醒,像上面那种线程数量很多的时候就比较复杂了,需要定义一个辅助类。

package multiplethread;

import java.util.concurrent.locks.LockSupport;

public class PrintDigitLetterUsingLockSupport {
    private static Thread digitThread, letterThread;

    public static void main(String[] args) {
        digitThread = new Thread(()->{
            for(int i=1; i<=26; i++){
                System.out.print(i);
                // 先阻塞等待唤醒
                LockSupport.park();
                // 唤醒下一个线程
                LockSupport.unpark(letterThread);
            }
        }, "digitThread");

        letterThread = new Thread(()->{
            for(int i=0; i<26; i++){
                System.out.print((char) (i+‘A‘));
                // 唤醒下一个线程
                LockSupport.unpark(digitThread);
                // 当前线程阻塞
                LockSupport.park();
            }
        }, "letterThread");

        digitThread.start();
        letterThread.start();
    }
}

BlockingQueue

阻塞队列 (BlockingQueue)Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。

BlockingQueue的主要方法:

put(E e): 将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
take():从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。

我们这里用BlockingQueue来实现上面的example3:用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z,思路就是用两个容量为1的阻塞队列,当数字打印线程digitThread打印一个数字后,往阻塞队列bq1中放入一个数,然后从阻塞队列bq2中取出一个数,此时bq2是空的,因此digitThread阻塞在这个线程上,而字母打印线程letterThread先从bq1中取出一个数,然后打印字母,再往bq2中放入一个数,此时digitThread因为bq2不为空,继续往下打印,而letterThread因为bq1已经为空了,也无法继续for循环,因此需要等待digitThread往bq1中放入数,才能不阻塞。

package multiplethread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class PrintDigitLetterUsingBlockingQueue{
    static BlockingQueue<Integer> bq1 = new ArrayBlockingQueue<>(1);
    static BlockingQueue<Integer> bq2 = new ArrayBlockingQueue<>(1);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=1; i<=26; i++){
                    try {
                        System.out.print(i);
                        bq1.put(1);
                        bq2.take();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<26; i++){
                    try{
                        bq1.take();
                        System.out.print((char)(i+‘A‘));
                        bq2.put(1);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

参考资料

多个线程顺序打印问题,一网打尽

LockSupport工具

理解Semaphore及其用法详解

用两个线程,一个输出字母,一个输出数字,交替输出1A2B3C4D...26Z

使用LockSupport实现线程交替打印1-100

多线程交互

原文:https://www.cnblogs.com/zhengxch/p/14729692.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!