java多线程入门篇 (三)

  • 锁与同步
  • 等待/通知机制
  • 信号量:
  • 管道:
  • 其它通信相关
  • join方法
  • sleep方法
  • ThreadLocal类
  • InheritableThreadLocal



java多线程入门篇(一)进程线程基本概念和入门类与接口


Java多线程入门篇(二)线程组线程优先级和线程状态

  在Java中合理的使用多线程可以更好的利用服务器资源。一般来说,线程都有自己的自由线程上下文,互不干扰。但是当我们需要多个线程之间互相协作的时候,就需要掌握Java线程的通信方式才可以

锁与同步

  在Java中锁都是基于对象的,所以又称为对象锁。锁与线程的关系就像婚姻关系,一个锁在同一时间只能由一个线程持有,只有当持有锁的线程释放锁后其它线程才能获得锁。
  同步就是让多个线程按照一定的顺序执行。而锁就是用于实现这个功能。
接下来我们看一个不加锁的程序:

public class Synchroniz {
    static class ThreadA implements Runnable{
        @Override
        public void run(){
            for (int i = 0;i<100;i++){
                System.out.println("线程A"+i);
            }
        }
    }
    static class ThreadB implements Runnable{
        @Override
        public void run(){
            for (int i = 0;i<100;i++){
                System.out.println("线程B"+i);
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
    }
}

一部分结果:

线程A9
线程A10
线程B0
线程A11
线程B1
线程A12
线程B2
线程A13

  从上面可以看出两个线程是各自独立工作的,输出自己的打印值。如果重新运行每次的结果都会不一样。
  接下来一个新的需求:希望A先执行完再执行B,那应该怎么办呢?最简单的方式就是使用一个“对象锁”。

实例代码:

public class Synchroniz1 {
    private static Object lock = new Object();
    static class ThreadA implements Runnable{
        @Override
        public void run(){
            synchronized (lock){
                for (int i = 0;i<100;i++){
                    System.out.println("线程A"+i);
                }
            }
        }
    }
    static class ThreadB implements Runnable{
        @Override
        public void run(){
            synchronized (lock){
                for (int i =0;i<100;i++){
                    System.out.println("线程B"+i);
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new ThreadA()).start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(new ThreadB()).start();

    }
}

部分结果:

线程A96
线程A97
线程A98
线程A99
线程B0
线程B1
线程B2

这里声明了一个名字为lock的对象锁。我们在ThreadA和ThreadB中需要同步的代码块里,都是用synchronized关键字加上了同一个对象锁lock。这样同一时间就只有一个线程可以持有一个锁。

等待/通知机制

  上面一种基于”锁“的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会消耗服务器资源,而等待/通知机制是另一种方式。
  Java的等待/通知机制是基于Object类的wait()方法和notify(),notifyAll()方法实现的。(notify()方法随机叫醒一个正在等待的线程,notifyAll()叫醒所有正在等待的线程。)
  我们知道一个锁同一时刻只能被一个线程持有,如果线程A持有了锁lock并开始执行,它可以调用lock.wait()让自己进入等待状态,此时lock锁是被释放了的。
  然后线程B获得了锁并执行,然后调用notify()唤醒之前持有锁的线程。
注意:唤醒不代表运行,不代表获得锁,还是需要等当前持有锁的线程释放锁之后,其它线程才可以去竞争获得锁。

代码实现:

public class Synchroniz2 {
    private static Object lock = new Object();
    static class ThreadA implements Runnable{
        @Override
        public void run(){
            synchronized (lock){
                for (int i = 0;i<5;i++){
                    System.out.println("ThreadA"+i);
                    try {
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }
    static class ThreadB implements Runnable{
        @Override
        public void run(){
            synchronized (lock){
                for (int i = 0;i<5;i++){
                    System.out.println("ThreadB"+i);
                    try {
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

ThreadA0
ThreadB0
ThreadA1
ThreadB1
ThreadA2
ThreadB2
ThreadA3
ThreadB3

  在上面的例子中,线程A,B都是先打印出自己需要打印的东西,然后唤醒另一个线程,自己再进入等待并释放锁。所以呈现的就是A,B线程交叉打印。
注意:等待/通知机制需要线程使用同一个对象锁,如果两个线程使用的是不同的对象锁,那他们是不可以使用等待/通知机制的。

信号量:

信号量 (Semaphore),有时被称为信号灯,是在 多线程 环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被 并发 调用。 在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。

  jdk提供了类似与”信号量“功能的类:Semaphore。不过接下来我要介绍的是基于volatile关键字自己实现信号量通信的方式。

volatile关键字能够保证内存可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其他线程是立马可见更改之后的值的。

需求:我希望线程A输出0,线程B输出1,线程A输出2,线程B输出3,一次类推,改如何做?

实例代码:

public class SemaphoreDemo {
    private static volatile int signal = 0;
    static class ThreadA implements Runnable{
        @Override
        public void run(){
            while (signal<5){
                if (signal%2==0){
                    System.out.println("ThreadA"+signal);
                    synchronized (this){
                        signal++;
                    }
                }
            }
        }
    }
    static class ThreadB implements Runnable{
        @Override
        public void run(){
            while (signal<5){
                if (signal%2==1){
                    System.out.println("ThreadB"+signal);
                    synchronized (this){
                        signal++;
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
    }
}

ThreadA0
ThreadB1
ThreadA2
ThreadB3
ThreadA4
ThreadB5

通过上面的代码我们使用volatile关键字实现了信号量模型。
注意:volatile变量需要进行原子操作,signal++并不是原子操作所以我们需要使用synchronized给他上锁。

管道:

管道是基于”管道流“的通信方式,jdk提供了PipedWriter,PipedReader,PipedOutputStream,PipedInputStream。其中,前面两个是基于字符的,后面两个是基于字符流的。
实例代码:

public class pipe {
    static class ReaderThread implements Runnable{
        private PipedReader reader;
        public ReaderThread(PipedReader reader){
            this.reader = reader;
        }
        @Override
        public void run(){
            System.out.println("this is reader");
            int receive = 0;
            try{
                while ((receive = reader.read())!= -1){
                    System.out.print((char)receive);
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
    static class WriterThread implements Runnable{
        private PipedWriter writer;
        public WriterThread(PipedWriter writer){
            this.writer = writer;
        }
        @Override
        public void run(){
            System.out.println("this is writer");
            int receive = 0;
            try {
                writer.write("test");
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try{
                    writer.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        writer.connect(reader);
        new Thread(new ReaderThread(reader)).start();
        Thread.sleep(1000);
        new Thread(new WriterThread(writer)).start();
    }
}

this is reader
this is writer
test

简单分析执行流程:
1:线程ReaderThread开始执行,
2:线程ReaderThread使用管道reader.read()进入阻塞
3:线程WriterThread开始执行
4:线程WriterThread执行writer.write(“test”),往管道写入字符串
5:线程WriterThread执行结束
6:线程ReaderThread读取管道输入的字符串并打印
7:线程ReaderThread执行结束。

其它通信相关

join方法

  join()方法是Thread类的一个实例方法,作用是让当前线程陷入等待状态。等待join的线程执行完成再继续执行。比如:在主线程中要执行一个子线程,此子线程耗费的时间比较久,如果正常执行会在主线程完成之后完成,此时要是希望子线程先完成则可以在主线程中使用"子线程.join()",使主线程等待子线程完成之后再继续执行。

实例代码:

public class Join {
    static class ThreadA implements Runnable{
        @Override
        public void run(){
            System.out.println("我是子线程,我先等一会");
            try {
                Thread.sleep(1000);
                System.out.println("ok了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new ThreadA());
            thread.start();
            thread.join();
            System.out.println("如果我前面有人说话就代表使用了join()管用了,不然应该我先说话");
        }
    }
}

我是子线程,我先等一会
ok了
如果我前面有人说话就代表使用join()管用了,不然应该我先说话

sleep方法

  sleep()是Thread类的一个静态方法,是让当前线程睡一会。
方法有:Thread.sleep(long),Thread.sleep(long,int)
注意:sleep方法是不会释放当前的锁的。而wait方法会。这也是常见的多线程的面试题。还有就是wait既可以指定时间也可以不指定时间,而sleep必须指定时间。

ThreadLocal类

  它是一个本地线程副本变量工具类,内部是一个弱引用的Map来维护。它是让每个线程有自己独立的变量,线程之间互不影响。
  如果开发者希望将类的某个静态变量与线程状态关联,则可以使用ThreadLocal。常见的使用场景为用来解决数据库连接,Session管理等。

InheritableThreadLocal

  它不仅仅是当前线程可以存取副本值,而且它的子线程也可以存取这个副本值。