多线程Thread

362 阅读16分钟

进程与线程

进程

  1. 是程序的一次执行;是操作系统资源分配的基本单位
  2. 进程有自己的独立地址空间,每启动一个进程,系统都会为其分配地址空间

线程

  1. 是进程中独立运行的子任务;是处理器进行任务调度的基本单元
  2. 同一进程下的线程共享全局变量、静态变量等数据

Java线程

新建线程方式

public class MyThread1 extends Thread{

    @Override
    public void run() {
        System.out.println("1");
    }
}
public class MyThread2 implements Runnable{

    @Override
    public void run() {
        System.out.println("2");
    }
}

使用继承Thread类的方式有局限性,因为java只能继承一个类,不支持多继承。使用实现Runnable接口的方式,可以额外的继承一个其它类。

线程常用方法

1. static Thread currentThread() 返回执行当前代码的线程对象
2. static void sleep(long millis) 执行当前代码的线程休眠
3. static void yield() 执行当前代码的线程让出CPU资源
4. void interrupt() 在调用该方法的线程对象(而不是执行当前代码的线程对象)打了一个停止标志(中断标志),并不是真的停止线程
5. static boolean interrupted() 返回执行当前代码的线程是否为中断状态,执行后会清除中断标志
6. boolean isInterrupted() 返回调用该方法的线程(而不是执行当前代码的线程对象)是否为中断状态,执行后不清除中断标志
7. long getId() 获取线程ID
8. int getPriority() 获取线程优先级  

线程分类

  • 用户线程 xx

  • 守护线程

  1. 特点 1)当进程中不在存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程。

2)守护线程中产生的新线程也是守护线程

3)守护线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。原因:一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。

  1. 设置 在调用线程对象的start()方法之前,调用setDaemon(true)方法设置成守护线程

线程状态

  • 状态类别
public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }
  • 状态转换图 2615789-1345e368181ad779.png

线程同步

  • 局部变量不存在线程安全问题,因此是线程安全的。
  • 实例变量是线程不安全的,因为十分有可能是该实例变量是共享的。

线程安全问题

是多线程访问了共享资源(资源竞争)会出现线程安全问题。因此需要在多线程执行到共享资源的时候让线程一个一个排队的去执行共享资源(同步执行)。

synchronized

  1. synchronized:阻塞可重入锁,可以修饰方法,也可以作用于代码块。用于同步共享资源。

    可重入锁:自己可以再次获取自己的内部锁,比如线程A获取到了obj对象的锁,此时这个对象锁还没有释放,当再次想要获取这个对象的锁的时候是可以获取的。试想一下,synchronized的锁是不可重入的就会造成死锁

  2. synchronized修饰非静态方法,则锁加在了当前对象上。修饰静态方法,则锁加在了Class对象上。synchronized(obj)修饰代码块,需要指定锁加在指定对象上,如果obj为this表示当前对象上;如果为Class对象,则加在Class对象上。请注意如果obj是String类型的常量,则特别注意常量池带来的例外。
  3. synchronized修饰的方法可以被子类继承,但是如果子类重新了父类的同步方法A,那么在导出类中重写的A仍然需要加上synchronized关键字同步,否则不具备同步性。
  4. synchronized修饰的同步代码,线程获取到锁后什么时候释放锁呢?1)执行完同步代码块 2)执行同步代码块时候遇到异常,并退出了同步代码块
  5. 每个锁对象都有两个队列,一个是阻塞队列,一个是就绪队列。阻塞队列存储了被阻塞的线程,就绪队列存储了将要获取锁的线程。

ReentrantLock

  • ReentrantLock能实现synchronized同步效果
 class CountThread extends Thread{

        ReentrantLock lock = new ReentrantLock();
        
        @Override
        public void run() {
            try {
                lock.lock();
                // ...
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
  • ReentrantLock + Condition(wait/single/singleAll)能实现通知等待机制 该方式最大优势是可以选择性的唤醒阻塞的线程。而synchronized + wait()/wait(long) + notify()/notifyAll()实现方式,要么由虚拟机随机唤醒一个线程,要么全部唤醒。
  • ReentrantReadWriteLock

ReentrantLock是线程全互斥的,即同一时间只有一个线程在执行共享资源,这样做虽然保证了实例变量的线程安全,但效率是非常低下的。ReentrantReadWriteLock可以加快运行效率,提升代码运行速度。

读写锁表示有两个锁,一个是读锁(共享锁),一个是写锁(排他锁)。多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。

volatile

作用

是使变量在多线程间可见。是线程同步轻量级实现。

如何保证可见性

线程访问被volatile修饰的变量,每次都从共享内存获取和写入

volatile与synchronized区别

  • 前者只能修饰变量;后者只能修饰方法和代码块
  • 前者不会阻塞线程;后者会阻塞线程
  • 前者能保证数据可见性,但不能保证原子性;后者可以保证可见性和原子性

死锁

不同的线程在等待根本不可能被释放的锁,导致所有的任务都无法继续完成

四大必要条件

  1. 不可剥夺

进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,只能由获得该资源的进程自己来释放

  1. 请求与保持

进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

  1. 互斥

进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

  1. 循环等待

有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。

检测死锁

jstack -l pid >> threadDump.txt

示例

public class DeadThread extends Thread{

    private boolean chooseFlag = false;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    @Override
    public void run() {
        if (chooseFlag){//第二个线程执行
            synchronized (lock1){
                System.out.println("chooseFlag=true获取到了锁lock1");
                try {
                    Thread.sleep(2000);//等待第一个线程获取lock2
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println("chooseFlag=true获取到了锁lock2");
                }
            }
        }else {//第一个线程执行
            synchronized (lock2){
                System.out.println("chooseFlag=false获取到了锁lock2");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1){
                    System.out.println("chooseFlag=false获取到了锁lock1");
                }
            }
        }
    }

    public void setChooseFlag(boolean chooseFlag) {
        this.chooseFlag = chooseFlag;
    }
}
public class App {
    public static void main( String[] args ) throws InterruptedException {
        DeadThread t = new DeadThread();

        Thread thread1 = new Thread(t);
        thread1.start();
        Thread.sleep(100);//等待启动第二个线程并获取到lock1锁

        t.setChooseFlag(true);
        Thread thread2 = new Thread(t);
        thread2.start();
    }
}

运行main方法,输出如下:

chooseFlag=false获取到了锁lock2
chooseFlag=true获取到了锁lock1

找到mian方法的PID

C:\Users\xxx\Desktop\test>jps
10432 RemoteMavenServer
10848
1012 App
12900 Jps
12632 Launcher

C:\Users\xxx\Desktop\test>jstack -l 1012 >> threadDump.txt

threadDump.txt部分内容,检测出有死锁

Found one Java-level deadlock:
=============================
"Thread-2":
  waiting to lock monitor 0x000000001cf21298 (object 0x000000076ba891e0, a java.lang.Object),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 0x000000001cf1ea08 (object 0x000000076ba891d0, a java.lang.Object),
  which is held by "Thread-2"

Java stack information for the threads listed above:
===================================================
"Thread-2":
	at org.example.DeadThread.run(DeadThread.java:20)
	- waiting to lock <0x000000076ba891e0> (a java.lang.Object)
	- locked <0x000000076ba891d0> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)
"Thread-1":
	at org.example.DeadThread.run(DeadThread.java:32)
	- waiting to lock <0x000000076ba891d0> (a java.lang.Object)
	- locked <0x000000076ba891e0> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

解决死锁问题

1.线程获取锁的时候加上一个时限,超过时限就线程就不再等待锁

2.银行家算法

线程间通信

等待通知机制

synchronized + wait()/wait(long) + notify()/notifyAll()

wait()/wait(long)
-在Object类中
-在获取到锁之后(在synchronized修饰的范围内)才能使用,否则跑异常
-执行后,当前线程释放锁,并且线程阻塞,使其进入阻塞队列
-阻塞线程被唤醒(notify/notifyAll)之后,会去获取锁,然后执行wait之后的代码
-如果线程执行了wait阻塞的时候,调用线程对象的interrupt方法则会抛出中断异常,即表现形式与sleep一样了

notify()/notifyAll()
-在Object类中
-在获取到锁之后(在synchronized修饰的范围内)才能使用,否则跑异常
-执行后,唤醒一个/全部线程,使其进入就绪队列,当前线程锁不释放,特别注意锁释放在执行完了同步代码或者同步代码中跑了异常之后才释放的

等待通知机制典型实践-生产者消费者模式

一生产者与一消费者

package org.example;

public class MyThread3 {

    static class Util{

        private String food;
        Object lock = new Object();

        public void set(String value){
            try {
                synchronized (lock) {
                    if (food != null) {
                        System.out.println(Thread.currentThread().getName()+":吧台还有菜, 厨师在等");
                        lock.wait();//要使用加锁的对象调该方法,否则就算是在同步代码内部,仍然会报IllegalMonitorStateException
                    }
                    System.out.println(Thread.currentThread().getName()+":厨师做好菜了:" + value);
                    food = value;
                    lock.notify();//要使用加锁的对象调该方法,否则就算是在同步代码内部,仍然会报IllegalMonitorStateException
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public void remove(){
            try {
                synchronized (lock) {
                    if (food == null) {
                        System.out.println(Thread.currentThread().getName()+":吧台没菜了, 顾客在等");
                        lock.wait();
                    }
                    System.out.println(Thread.currentThread().getName()+":顾客吃完菜了:" + food);
                    food = null;
                    lock.notify();
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

   static class ProducerThread extends Thread{

       private Util util;

       public ProducerThread(String name, Util util){
           super(name);
           this.util = util;
       }

       @Override
       public void run() {
           while (true) {
               util.set("马汉全席套餐");
           }
       }
   }

   static class ConsumerThread extends Thread{

        private Util util;

       public ConsumerThread(String name, Util util){
           super(name);
           this.util = util;
       }

        @Override
        public void run() {
           while (true) {
               util.remove();
           }
        }
    }

   public static void main(String[] args) {
       Util util = new Util();
       new ProducerThread("p-1", util).start();
       new ConsumerThread("c-1",util).start();
   }
}

输出

p-1:厨师做好菜了:马汉全席套餐
p-1:吧台还有菜, 厨师在等
c-1:顾客吃完菜了:马汉全席套餐
c-1:吧台没菜了, 顾客在等
p-1:厨师做好菜了:马汉全席套餐
p-1:吧台还有菜, 厨师在等
c-1:顾客吃完菜了:马汉全席套餐
c-1:吧台没菜了, 顾客在等
...

多生产者与多消费者

程序假死怎么出现的,如何避免

与一生产者和一消费者区别:1)Util类中将if替换成了while 2)mian方法中生产者和消费者各启动了两个

package org.example;

public class MyThread4 {

    static class Util{

        private String food;
        Object lock = new Object();

        public void set(String value){
            try {
                synchronized (lock) {
                    while (food != null) {
                        System.out.println(Thread.currentThread().getName()+":吧台还有菜, 厨师在等");
                        lock.wait();//要使用加锁的对象调该方法,否则就算是在同步代码内部,仍然会报IllegalMonitorStateException
                    }
                    System.out.println(Thread.currentThread().getName()+":厨师做好菜了:" + value);
                    food = value;
                    lock.notify();//要使用加锁的对象调该方法,否则就算是在同步代码内部,仍然会报IllegalMonitorStateException
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public void remove(){
            try {
                synchronized (lock) {
                    while (food == null) {
                        System.out.println(Thread.currentThread().getName()+":吧台没菜了, 顾客在等");
                        lock.wait();
                    }
                    System.out.println(Thread.currentThread().getName()+":顾客吃完菜了:" + food);
                    food = null;
                    lock.notify();
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

   static class ProducerThread extends Thread{

       private Util util;

       public ProducerThread(String name, Util util){
           super(name);
           this.util = util;
       }

       @Override
       public void run() {
           while (true) {
               util.set("马汉全席套餐");
           }
       }
   }

   static class ConsumerThread extends Thread{

        private Util util;

       public ConsumerThread(String name, Util util){
           super(name);
           this.util = util;
       }

        @Override
        public void run() {
           while (true) {
               util.remove();
           }
        }
    }

   public static void main(String[] args) throws InterruptedException {
       Util util = new Util();

       for (int i=1; i<3; i++) {
           new ProducerThread("p-"+i, util).start();
       }
       for (int i=1; i<3; i++) {
           new ConsumerThread("c-"+i, util).start();
       }

       Thread.sleep(10000);//等待10s,打印出各个线程状态
       int threadCount = Thread.currentThread().getThreadGroup().activeCount();
       Thread[] threads = new Thread[threadCount];
       Thread.currentThread().getThreadGroup().enumerate(threads);
       for (Thread t: threads) {
           System.out.println(t.getName()+" "+t.getState());
       }

   }
}

输出

p-1:厨师做好菜了:马汉全席套餐
p-1:吧台还有菜, 厨师在等
p-2:吧台还有菜, 厨师在等
c-1:顾客吃完菜了:马汉全席套餐
c-1:吧台没菜了, 顾客在等
p-1:厨师做好菜了:马汉全席套餐
p-1:吧台还有菜, 厨师在等
p-2:吧台还有菜, 厨师在等
c-2:顾客吃完菜了:马汉全席套餐
c-2:吧台没菜了, 顾客在等
c-1:吧台没菜了, 顾客在等
main RUNNABLE
Monitor Ctrl-Break RUNNABLE
p-1 WAITING
p-2 WAITING
c-1 WAITING
c-2 WAITING

从上述结果可以看到:各个线程最终都处于WAITING状态,不能在运行下去,即程序假死。假死是由于唤醒线程使用notify只会唤起阻塞队列中一个线程,这个时候可能唤起的是同类线程(如生产者唤起生产者,此时会使得被唤起的生产者阻塞),这种比率积少成多的时候就会导致假死。

解决上述线程假死的方法是:使用notifyAll代替notify,去唤醒所有线程即可。

管道流通信

管道流用于在不同线程间直接传送(不需要结束临时文件之类)数据。一个线程发送数据到输出管道,另一个线程从输入管道中读取数据。

 管道字节流:PipedInputStream & PipedOutputStream
 管道字符流:PipedWriter & PipedReader

字节流示例,字符流类似

package org.example;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class MyThread5 {

    static class Util{
        public void writeData(PipedOutputStream pipedOutputStream, String content){
            try {
                pipedOutputStream.write(content.getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if (pipedOutputStream != null){
                    try {
                        pipedOutputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        public String readData(PipedInputStream pipedInputStream){
            StringBuilder stringBuilder = new StringBuilder();
            try {
                byte[] arr = new byte[1024];
                int length;
                while ((length = pipedInputStream.read(arr)) != -1) {
                    stringBuilder.append(new String(arr, 0, length));
                }
            }catch (IOException e){
                e.printStackTrace();
            }finally {
                if (pipedInputStream != null){
                    try {
                        pipedInputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return stringBuilder.toString();
        }
    }

    static class WriteThread extends Thread{

        private Util util;
        private PipedOutputStream pipedOutputStream;

        public WriteThread(String name, Util util, PipedOutputStream pipedOutputStream){
            super(name);
            this.util = util;
            this.pipedOutputStream = pipedOutputStream;
        }

        @Override
        public void run() {
            String msg = "的幅度萨芬方萨克发第三方黄范德萨";
            System.out.println(Thread.currentThread().getName()+"写入的数据:"+ msg);
            util.writeData(pipedOutputStream, msg);
        }
    }

    static class ReadThread extends Thread{

        private Util util;
        private PipedInputStream pipedInputStream;

        public ReadThread(String name, Util util, PipedInputStream pipedInputStream){
            super(name);
            this.util = util;
            this.pipedInputStream = pipedInputStream;
        }

        @Override
        public void run() {
            String data = util.readData(pipedInputStream);
            System.out.println(Thread.currentThread().getName()+"读取的数据:"+ data);
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Util util = new Util();

        //管道流,并输入输出连接上
        PipedInputStream pipedInputStream = new PipedInputStream();
        PipedOutputStream pipedOutputStream = new PipedOutputStream();
        pipedInputStream.connect(pipedOutputStream);

        new WriteThread("p-1", util, pipedOutputStream).start();
        Thread.sleep(1000);//等待数据被写入管道输出流
        new ReadThread("c-1", util, pipedInputStream).start();
    }
}

输出

p-1写入的数据:的幅度萨芬方萨克发第三方黄范德萨
c-1读取的数据:的幅度萨芬方萨克发第三方黄范德萨

join

使用场景

在很多情况下,主线程创建并启动子线程,如果子线程中药要进行大量的耗时运算,主线程往往将早于子线程之前结束。如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值。通过join方法可以实现。

请注意: join()/join(long)方法是通过wait方法实现的,并且该方法是同步方法,源码伪代码如下所示。因为wait方法在线程被中断的时候会抛出异常,因此join在线程中断的时候也会抛出异常。

 public final synchronized void join(long millis)
    throws InterruptedException {
     //...
     wait
     //...
 }

线程私有数据

除了之前说到了局部变量是天然线程私有的,还有一种方式可以实现线程私有,即ThreadLocal&InheritableThreadLocal。

ThreadLocal

每个线程都可以绑定自己的值到ThreadLocal,并且该值是线程私有的,在整个当前线程中是可见的。

ThreadLocal存储的私有变量不具备继承性,即父线程中的存储的变量,在子线程中是不可见的,反之,也成立。父子线程各自有各自所拥有的值。

InheritableThreadLocal

每个线程都可以绑定自己的值到InheritableThreadLocal,并且该值是线程私有的,在整个当前线程和子线程中是可见的。

InheritableThreadLocal存储的私有变量具备继承性。同时也可以做到值继承再修改。

值继承再修改: 通过自定义ThreadLocal并覆盖父类的下面的方法实现:

protected T childValue(T parentValue) {
        return parentValue;
    }

比如MyThreadLocal类

protected T childValue(T parentValue) {
        return "我是子线程,"+parentValue;
    }

则每次通过get获取值的时候,都会在父线程对应的值前面加上"我是子线程,"并返回

定时器Timer

常用API

1. 执行一次
schedule(TimerTask task, long delay)
schedule(TimerTask task, Date time)
2. 周期执行
schedule(TimerTask task, long delay, long period)
schedule(TimerTask task, Date firstTime, long period)
3. 周期执行
scheduleAtFixedRate(TimerTask task, long delay, long period)
scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

特性和区别

周期执行schedule与scheduleAtFixedRate的特性和区别

  • schedule执行的任务的时间没有被延时,则下一次任务执行的时间是上一次任务开始的时间+period表示的时间;如果执行的任务的时间被延时了,则下次任务执行的时间是上一次任务结束的时间

  • scheduleAtFixedRate不管执行的任务的时间没有被延时,则下次任务执行的时间都是上一次任务结束的时间

  • scheduleAtFixedRate具有任务追赶执行的特性。schedule不具有。如果任务指定开始的时间早于第一次执行任务的时间,那么本来在这段时间内的任务,schedule直接忽略。scheduleAtFixedRate会补充执行。

  • schedule(TimerTask task, Date firstTime, long period)和scheduleAtFixedRate(TimerTask task, Date firstTime, long period)如果指定的firstTime早于当前时间,则会立即执行