Java多线程系列(1)--- 基础篇

1. 基本概念

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

线程状态图
在这里插入图片描述
说明:

  1. 新建状态(New):线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  2. 就绪状态(Runnable):也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态(Running) :线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked) :阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
    (02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    (03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead) :线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

2. 实现多线程的两种常用方式

常用方式一: 继承Thread类

// Thread 是一个类。Thread本身就实现了Runnable接口。声明如下:
public class Thread implements Runnable {		}

代码示例:

// 自定义类继承Thread类
public class MyThread extends Thread {
    private int tickets = 50;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(this.getName() + "正在出售第" + tickets + "张票!");
                tickets--;
            } else {
                break;
            }
        }
    }
}
// 测试
public class TestThread {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.setName("窗口一");
        MyThread t2 = new MyThread();
        t2.setName("窗口二");
        MyThread t3 = new MyThread();
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}

// 结果
三个线程各自卖了50张票

常用方式二: 实现Runnable接口

// Runnable 是一个接口,该接口中只包含了一个run()方法。定义如下:
public interface Runnable {
    public abstract void run();
}

代码示例:

// 自定义类实现Runnable接口
public class MyThread implements Runnable {
    private int tickets = 100;
    Object lock = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票.");
                    tickets--;
                } else {
                    break;
                }
            }
        }
    }
}

// 测试
public class TestThread {
    public static void main(String[] args) {
        MyThread task = new MyThread();

        Thread t1 = new Thread(task, "窗口一");
        Thread t2 = new Thread(task, "窗口二");
        Thread t3 = new Thread(task, "窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}

// 结果
三个窗口共同卖100张票

Thread和Runnable的异同点
相同点:都是多线程的实现方式
不同点:Thread 是类,而Runnable是接口;Thread本身是实现了Runnable接口的类。“一个类只能有一个父类,但是却能实现多个接口”,因此Runnable具有更好的扩展性。此外,Runnable还可以用于"资源的共享"。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。通常,建议通过"Runnable"实现多线程!

3. Thread中start()和run()

start() 和 run()相关源码

/* What will be run. */
private Runnable target;

/* The group of this thread */
private ThreadGroup group;

/* 
 * Java线程状态,初始化值为0,表示线程未启动
 */
private volatile int threadStatus = 0;

public synchronized void start() {
      /**
       * 主方法线程或“系统”不能调用此方法,线程组被VM创建。
       * 此方法以后如果添加了新功能,需要添加到VM中
       *
       * A zero status value corresponds to state "NEW".
       */
      if (threadStatus != 0)
          throw new IllegalThreadStateException();

      /* 
       * 通知线程组此线程即将启动,此线程会被添加到线程组的列表中,线程组中未启动数量会减一
       */
      group.add(this);

      boolean started = false;
      try {
          start0();
          started = true;
      } finally {
          try {
              if (!started) {
                  group.threadStartFailed(this);
              }
          } catch (Throwable ignore) {
              /* do nothing. If start0 threw a Throwable then
                it will be passed up the call stack */
          }
      }
  }

private native void start0();
/**
 * 此方法用来启动线程
 * <p>
 * Subclasses of <code>Thread</code> should override this method.
 *
 * @see     #start()
 * @see     #stop()
 * @see     #Thread(ThreadGroup, Runnable, String)
 */
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

说明:start()实际上是通过本地方法start0()启动线程的。而start0()会新运行一个线程,新线程会调用run()方法。target是一个Runnable对象。run()就是直接调用Thread线程的Runnable成员的run()方法,并不会新建一个线程。
start() 和 run()的区别说明
start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。注意start()不能被重复调用。
run() : run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),不会启动新线程!

start() 和 run()的区别示例

public class MyThread extends Thread {

    public MyThread(String name) {
        super(name);
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在执行。。。");
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread thread = new MyThread("MyThread线程");

        System.out.println(Thread.currentThread().getName() + "执行thread.run()");
        thread.run();
        System.out.println("---------分割线----------");
        System.out.println(Thread.currentThread().getName() + "执行thread.start()");
        thread.start();
    }
}

// 结果
main执行thread.run()
main正在执行。。。
---------分割线----------
main执行thread.start()
MyThread线程正在执行。。。

结果说明:
(01) Thread.currentThread().getName()是用于获取“当前线程”的名字。当前线程是指正在cpu中调度执行的线程。
(02) thread.run()是在“主线程main”中调用的,该run()方法直接运行在“主线程main”上。
(03) thread.start()会启动“线程mythread”,“线程mythread”启动之后,会调用run()方法;此时的run()方法是运行在“线程mythread”上。

扫描二维码关注公众号,回复: 9062988 查看本文章

4. synchronized关键字

synchronized原理
java中,每一个对象有且仅有一个同步锁。这意味着,同步锁是依赖于对象而存在。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了“obj这个对象”的同步锁。不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如:
现在有两个线程:A和B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。

synchronized基本规则
(1): 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
代码示例一:

public class MyRunnable implements Runnable {
    
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "---" + i);
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        MyRunnable task = new MyRunnable();
        Thread t1 = new Thread(task,"线程一");
        Thread t2 = new Thread(task,"线程二");
        t1.start();
        t2.start();
    }
}

// 运行结果
线程一---0
线程一---1
线程一---2
线程一---3
线程一---4
线程二---0
线程二---1
线程二---2
线程二---3
线程二---4

结果说明:
run()方法中存在“synchronized(this)代码块”,而且t1和t2都是基于"task这个Runnable对象"创建的线程。这就意味着,我们可以将synchronized(this)中的this看作是“task这个Runnable对象”;因此,线程t1和t2共享“task对象的同步锁”。所以,当一个线程运行的时候,另外一个线程必须等待“运行线程”释放“task的同步锁”之后才能运行。

代码示例二:

public class MyThread extends Thread {

    public MyThread(String name) {
        super(name);
    }

    public void run() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "---" + i);
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {

        MyThread t1 = new MyThread("线程一");
        MyThread t2 = new MyThread("线程二");
        t1.start();
        t2.start();
    }
}

// 运行结果:
线程一---0
线程二---0
线程二---1
线程一---1
线程一---2
线程二---2
线程二---3
线程一---3
线程一---4
线程二---4

结果说明:
synchronized(this)中的this是指“当前的类对象”,即synchronized(this)所在的类对应的当前对象。它的作用是获取“当前对象的同步锁”。对于代码示例二中,synchronized(this)中的this代表的是MyThread对象,而t1和t2是两个不同的MyThread对象,因此t1和t2在执行synchronized(this)时,获取的是不同对象的同步锁。对于代码示例一而言,synchronized(this)中的this代表的是MyRunable对象;t1和t2共同一个MyRunable对象,因此,一个线程获取了对象的同步锁,会造成另外一个线程等待。

(2): 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。
代码示例:

public class Apple {

    public void asynchronousMethod() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "非同步方法 循环" + i);
        }
    }

    public void synchronizedMethod() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "同步方法 循环" + i);
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {

        final Apple apple = new Apple();

        Thread t1 = new Thread(new Runnable() {
            public void run() {
                apple.synchronizedMethod();
            }
        }, "线程一");
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                apple.asynchronousMethod();
            }
        }, "线程二");

        t1.start();
        t2.start();
    }
}

// 运行结果:
线程二非同步方法 循环0
线程一同步方法 循环0
线程一同步方法 循环1
线程二非同步方法 循环1
线程二非同步方法 循环2
线程一同步方法 循环2
线程二非同步方法 循环3
线程一同步方法 循环3
线程二非同步方法 循环4
线程一同步方法 循环4

结果说明:
主线程中新建了两个子线程t1和t2。t1会调用apple对象的synchronizedMethod()方法,该方法内含有同步块;而t2则会调用apple对象的asynchronousMethod()方法,该方法不是同步方法。t1运行时,虽然调用synchronized(this)获取“apple的同步锁”;但是并没有造成t2的阻塞,因为t2没有用到“apple”同步锁。

(3): 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
代码示例:

public class Apple {

    public void synchronizedMethod() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "同步方法1 循环" + i);
            }
        }
    }

    public void synchronizedMethod2() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "同步方法2 循环" + i);
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {

        final Apple apple = new Apple();

        Thread t1 = new Thread(new Runnable() {
            public void run() {
                apple.synchronizedMethod();
            }
        }, "线程一");
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                apple.synchronizedMethod2();
            }
        }, "线程二");

        t1.start();
        t2.start();
    }
}

// 运行结果:
线程一同步方法1 循环0
线程一同步方法1 循环1
线程一同步方法1 循环2
线程一同步方法1 循环3
线程一同步方法1 循环4
线程二同步方法2 循环0
线程二同步方法2 循环1
线程二同步方法2 循环2
线程二同步方法2 循环3
线程二同步方法2 循环4

结果说明:
主线程中新建了两个子线程t1和t2。t1和t2运行时都调用synchronized(this),这个this是Apple对象(apple),而t1和t2共用apple。因此,在t1运行时,t2会被阻塞,等待t1运行释放“apple对象的同步锁”,t2才能运行。

synchronized方法 和 synchronized代码块
“synchronized方法”是用synchronized修饰方法,而 “synchronized代码块”则是用synchronized修饰代码块。

synchronized方法代码示例:

    public synchronized void synchronizedMethod(){
        System.out.println("这是一个同步方法");
    }

synchronized代码块代码示例:
synchronized代码块中的this是指当前对象。也可以将this替换成其他对象,例如将this替换成obj,则synchronizedCodeBlockMethod()在执行synchronized(obj)时就获取的是obj的同步锁。

    public void synchronizedCodeBlockMethod(){
        synchronized (this){
            System.out.println("这是一个同步代码块");
        }
    }

synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现更高效率。下面通过一个示例来演示:

public class Demo {
    public static void main(String[] args) {
        Demo demo = new Demo();
        long start, timeDiff;
        start = System.currentTimeMillis();
        demo.synchronizedMethod();
        timeDiff = System.currentTimeMillis() - start;
        System.out.println(timeDiff);

        start = System.currentTimeMillis();
        demo.synchronizedCodeBlockMethod();
        timeDiff = System.currentTimeMillis() - start;
        System.out.println(timeDiff);
    }

    public synchronized void synchronizedMethod() {
        for (int i = 0; i < 1000000; i++)
            ;
    }

    public void synchronizedCodeBlockMethod() {
        synchronized (this) {
            for (int i = 0; i < 1000000; i++)
                ;
        }
    }
}

其中一次执行结果:
16
4

实例锁和全局锁
实例锁 – 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。实例锁对应的就是synchronized关键字。
全局锁 – 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。

关于“实例锁”和“全局锁”的案例:

public class Banana {

    public synchronized void synchronizedMethodA(){}
    public synchronized void synchronizedMethodB(){}
    public static synchronized void classSynchronizedMethodA(){}
    public static synchronized void classSynchronizedMethodB(){}

}

假设,Banana 有两个实例a和b。分析下面4组表达式获取的锁的情况(是否能同时被访问)。
(01) a.synchronizedMethodA()与a.synchronizedMethodB()
(02) a.synchronizedMethodA()与b.synchronizedMethodA()
(03) a.classSynchronizedMethodA()与b.classSynchronizedMethodB()
(04) a.synchronizedMethodA()与Something.classSynchronizedMethodA()

(01) 不能被同时访问。因为synchronizedMethodA()和synchronizedMethodB()都是访问同一个对象(对象a)的同步锁!代码示例如下:

public class Banana {

    public synchronized void synchronizedMethodA() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "同步方法A 循环" + i);
        }
    }

    public synchronized void synchronizedMethodB() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "同步方法B 循环" + i);
        }
    }
}

public class LockDemo {

    Banana a = new Banana();

    private void test() {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                a.synchronizedMethodA();
            }
        }, "线程一");

        Thread t2 = new Thread(new Runnable() {
            public void run() {
                a.synchronizedMethodB();
            }
        }, "线程二");

        t1.start();
        t2.start();
    }

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        lockDemo.test();
    }
}

运行结果:
线程一同步方法A 循环0
线程一同步方法A 循环1
线程一同步方法A 循环2
线程一同步方法A 循环3
线程一同步方法A 循环4
线程二同步方法B 循环0
线程二同步方法B 循环1
线程二同步方法B 循环2
线程二同步方法B 循环3
线程二同步方法B 循环4

(02) 可以同时被访问。因为访问的不是同一个对象的同步锁,a.synchronizedMethodA()访问的是a的同步锁,而b.synchronizedMethodA()访问的是b的同步锁。代码示例如下:

public class Banana {

    public synchronized void synchronizedMethodA() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "同步方法A 循环" + i);
        }
    }

    public synchronized void synchronizedMethodB() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "同步方法B 循环" + i);
        }
    }
}

public class LockDemo {

    Banana a = new Banana();
    Banana b = new Banana();

    private void test() {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                a.synchronizedMethodA();
            }
        }, "线程一");

        Thread t2 = new Thread(new Runnable() {
            public void run() {
                b.synchronizedMethodA();
            }
        }, "线程二");

        t1.start();
        t2.start();
    }

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        lockDemo.test();
    }
}

其中一次运行结果:
线程二同步方法A 循环0
线程一同步方法A 循环0
线程二同步方法A 循环1
线程一同步方法A 循环1
线程二同步方法A 循环2
线程一同步方法A 循环2
线程一同步方法A 循环3
线程二同步方法A 循环3
线程一同步方法A 循环4
线程二同步方法A 循环4

(03) 不能被同时访问。因为classSynchronizedMethodA()和classSynchronizedMethodB()都是static类型,a.classSynchronizedMethodA()相当于Banana.classSynchronizedMethodA(),b.classSynchronizedMethodB()相当于Banana.classSynchronizedMethodB(),因此它们共用一个同步锁,不能被同时访问,代码示例如下:

public class Banana {

    public synchronized void synchronizedMethodA() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "同步方法A 循环" + i);
        }
    }

    public synchronized void synchronizedMethodB() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "同步方法B 循环" + i);
        }
    }

    public static synchronized void classSynchronizedMethodA(){
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "类同步方法A 循环" + i);
        }

    }
    public static synchronized void classSynchronizedMethodB(){
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "类同步方法B 循环" + i);
        }
    }
}

public class LockDemo {

    Banana a = new Banana();
    Banana b = new Banana();

    private void test() {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                a.classSynchronizedMethodA();
            }
        }, "线程一");

        Thread t2 = new Thread(new Runnable() {
            public void run() {
                b.classSynchronizedMethodB();
            }
        }, "线程二");

        t1.start();
        t2.start();
    }

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        lockDemo.test();
    }
}

其中一次执行结果:
线程一类同步方法A 循环0
线程一类同步方法A 循环1
线程一类同步方法A 循环2
线程一类同步方法A 循环3
线程一类同步方法A 循环4
线程二类同步方法B 循环0
线程二类同步方法B 循环1
线程二类同步方法B 循环2
线程二类同步方法B 循环3
线程二类同步方法B 循环4

(04) 可以被同时访问。因为synchronizedMethodA()是实例方法,a.synchronizedMethodA()使用的是对象a的锁;而classSynchronizedMethodA()是静态方法,Banana.classSynchronizedMethodA()可以理解为使用的是“类的锁”。因此,它们是可以被同时访问的。代码示例如下:

public class Banana {

    public synchronized void synchronizedMethodA() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "同步方法A 循环" + i);
        }
    }

    public synchronized void synchronizedMethodB() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "同步方法B 循环" + i);
        }
    }

    public static synchronized void classSynchronizedMethodA(){
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "类同步方法A 循环" + i);
        }

    }
    public static synchronized void classSynchronizedMethodB(){
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "类同步方法B 循环" + i);
        }
    }
}

public class LockDemo {

    Banana a = new Banana();
    Banana b = new Banana();

    private void test() {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                a.synchronizedMethodA();
            }
        }, "线程一");

        Thread t2 = new Thread(new Runnable() {
            public void run() {
                Banana.classSynchronizedMethodA();
            }
        }, "线程二");

        t1.start();
        t2.start();
    }

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        lockDemo.test();
    }
}

执行一次的结果:
线程一同步方法A 循环0
线程二类同步方法A 循环0
线程一同步方法A 循环1
线程二类同步方法A 循环1
线程一同步方法A 循环2
线程二类同步方法A 循环2
线程二类同步方法A 循环3
线程一同步方法A 循环3
线程二类同步方法A 循环4
线程一同步方法A 循环4

5. 线程等待与唤醒

wait(), notify(), notifyAll()方法介绍
Object类中,定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

Object类中关于等待/唤醒的API详细信息如下:

notify()        	           -- 唤醒在此对象监视器上等待的单个线程。
notifyAll()                    -- 唤醒在此对象监视器上等待的所有线程。
wait()                         -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。
wait(long timeout)             -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
wait(long timeout, int nanos)  -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。

wait()和notify()代码示例

public class ThreadA extends Thread{

    public ThreadA(String name){
        super(name);
    }

    public void run(){
        System.out.println(Thread.currentThread().getName() + "唤醒被阻塞的线程");
        notify();
    }
}

public class WaitDemo {
    public static void main(String[] args) {
        ThreadA t1 = new ThreadA("线程一");
        synchronized (t1){
            try {
                System.out.println(Thread.currentThread().getName() + "线程启动线程一");
                t1.start();
                System.out.println(Thread.currentThread().getName() + "线程进入阻塞状态");
                t1.wait();
                System.out.println(Thread.currentThread().getName() + "线程继续运行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:
main线程启动线程一
main线程进入阻塞状态
线程一唤醒被阻塞的线程
main线程继续运行

结果说明:
(01) 注意,图中"主线程" 代表“主线程main”。“线程t1” 代表WaitTest中启动的“线程t1”。 而“锁” 代表“t1这个对象的同步锁”。
(02) “主线程”通过 new ThreadA(“线程一”) 新建“线程t1”。随后通过synchronized(t1)获取“t1对象的同步锁”。然后调用t1.start()启动“线程t1”。
(03) “主线程”执行t1.wait() 释放“t1对象的锁”并且进入“等待(阻塞)状态”。等待t1对象上的线程通过notify() 或 notifyAll()将其唤醒。
(04) “线程t1”运行之后,通过synchronized(this)获取“当前对象的锁”;接着调用notify()唤醒“当前对象上的等待线程”,也就是唤醒“主线程”。
(05) “线程t1”运行完毕之后,释放“当前对象的锁”。紧接着,“主线程”获取“t1对象的锁”,然后接着运行。

注意: 为什么去掉了notify()之后,程序确实也能正常执行,那是因为用了 线程实例t1作为同步锁,因为当线程的run方法执行完毕,进入死亡状态的时,会调用线程实例的notifyAll方法(也有可能是notify方法)。这个可以参考线程的join方法源码。join方法的本质就是调用线程实例的wait方法,当线程执行完毕后,join方法会被唤醒,即wait方法被唤醒,本质上肯定调用过线程实例的notify方法。)
在这里插入图片描述
问题:t1.wait()应该是让“线程t1”等待;但是,为什么却是让“主线程main”等待了呢?
jdk文档中关于wait的一段介绍:

Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. 
In other words, this method behaves exactly as if it simply performs the call wait(0).
The current thread must own this object's monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object's monitor to wake up either through a call to the notify method or the notifyAll method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution.

引起“当前线程”等待,直到另外一个线程调用notify()或notifyAll()唤醒该线程。换句话说,这个方法和wait(0)的效果一样!(补充,对于wait(long millis)方法,当millis为0时,表示无限等待,直到被notify()或notifyAll()唤醒)。
“当前线程”在调用wait()时,必须拥有该对象的同步锁。该线程调用wait()之后,会释放该锁;然后一直等待直到“其它线程”调用对象的同步锁的notify()或notifyAll()方法。然后,该线程继续等待直到它重新获取“该对象的同步锁”,然后就可以接着运行。

jdk的解释中,说wait()的作用是让“当前线程”等待,而“当前线程”是指正在cpu上运行的线程!
这也意味着,虽然t1.wait()是通过“线程t1”调用的wait()方法,但是调用t1.wait()的地方是在“主线程main”中。而主线程必须是“当前线程”,也就是运行状态,才可以执行t1.wait()。所以,此时的“当前线程”是“主线程main”!因此,t1.wait()是让“主线程”等待,而不是“线程t1”!

wait(long timeout)和notify()代码示例

public class ThreadA extends Thread{

    public ThreadA(String name){
        super(name);
    }

    public void run(){
        System.out.println(Thread.currentThread().getName() + "无限running");
        while (true){

        }
    }
}

public class WaitDemo {
    public static void main(String[] args) {
        ThreadA t1 = new ThreadA("线程一");
        synchronized (t1){
            try {
                System.out.println(Thread.currentThread().getName() + "线程启动线程一");
                t1.start();
                System.out.println(Thread.currentThread().getName() + "线程进入阻塞状态");
                t1.wait(3000);
                System.out.println(Thread.currentThread().getName() + "线程继续运行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:
main线程启动线程一
main线程进入阻塞状态
线程一无限running  
main线程继续运行 // 大约3秒之后...输出“main线程继续运行”

结果说明:
如下图,说明了“主线程”和“线程t1”的流程。
(01) 注意,图中"主线程" 代表WaitTest主线程(即,线程main)。“线程t1” 代表WaitTest中启动的线程t1。 而“锁” 代表“t1这个对象的同步锁”。
(02) 主线程main执行t1.start()启动“线程t1”。
(03) 主线程main执行t1.wait(3000),此时,主线程进入“阻塞状态”。需要“用于t1对象锁的线程通过notify() 或者 notifyAll()将其唤醒” 或者 “超时3000ms之后”,主线程main才进入到“就绪状态”,然后才可以运行。
(04) “线程t1”运行之后,进入了死循环,一直不断的运行。
(05) 超时3000ms之后,主线程main会进入到“就绪状态”,然后接着进入“运行状态”。
在这里插入图片描述
wait() 和 notifyAll()代码示例
通过前面的示例,我们知道 notify() 可以唤醒在此对象监视器上等待的单个线程。下面,我们通过示例演示notifyAll()的用法;它的作用是唤醒在此对象监视器上等待的所有线程。

public class NotifyAllDemo {

    private static Object obj = new Object();

    public static void main(String[] args) {
        ThreadA t1 = new ThreadA("t1");
        ThreadA t2 = new ThreadA("t2");
        ThreadA t3 = new ThreadA("t3");
        t1.start();
        t2.start();
        t3.start();

        try {
            System.out.println(Thread.currentThread().getName() + "睡三秒");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + "被唤醒");
            obj.notifyAll();
        }
    }



    static class ThreadA extends Thread{

        public ThreadA(String name){
            super(name);
        }
        public void run(){
            synchronized (obj){
                try {
                    System.out.println(Thread.currentThread().getName() + "等待阻塞");
                    obj.wait();
                    System.out.println(Thread.currentThread().getName() + "继续执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

}

运行结果:
main睡三秒
t1等待阻塞
t2等待阻塞
t3等待阻塞
main被唤醒
t3继续执行
t2继续执行
t1继续执行

结果说明:
参考下面的流程图。
(01) 主线程中新建并且启动了3个线程"t1", “t2"和"t3”。
(02) 主线程通过sleep(3000)休眠3秒。在主线程休眠3秒的过程中,我们假设"t1", "t2"和"t3"这3个线程都运行了。以"t1"为例,当它运行的时候,它会执行obj.wait()等待其它线程通过notify()或额nofityAll()来唤醒它;相同的道理,“t2"和"t3"也会等待其它线程通过nofity()或nofityAll()来唤醒它们。
(03) 主线程休眠3秒之后,接着运行。执行 obj.notifyAll() 唤醒obj上的等待线程,即唤醒"t1”, "t2"和"t3"这3个线程。 紧接着,主线程的synchronized(obj)运行完毕之后,主线程释放“obj锁”。这样,“t1”, "t2"和"t3"就可以获取“obj锁”而继续运行了!
在这里插入图片描述
为什么notify(), wait()等函数定义在Object中,而不是Thread中
Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。
wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!OK,线程调用wait()之后,会释放它锁持有的“同步锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyAll()唤醒。现在,请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”。
负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放了“对象的同步锁”之后,等待线程才能获取到“对象的同步锁”进而继续运行。
总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。

6. 线程让步

yield()介绍
yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

yield()代码示例

 1 // YieldTest.java的源码
 2 class ThreadA extends Thread{
 3     public ThreadA(String name){ 
 4         super(name); 
 5     } 
 6     public synchronized void run(){ 
 7         for(int i=0; i <10; i++){ 
 8             System.out.println(Thread.currentThread().getName() + "循环" + i);
 9             // i整除4时,调用yield
10             if (i%4 == 0)
11                 Thread.yield();
12         } 
13     } 
14 } 
15 
16 public class YieldTest{ 
17     public static void main(String[] args){ 
18         ThreadA t1 = new ThreadA("t1"); 
19         ThreadA t2 = new ThreadA("t2"); 
20         t1.start(); 
21         t2.start();
22     } 
23 } 

其中一次执行结果:
线程一循环0
线程一循环1
线程二循环0
线程一循环2
线程二循环1
线程一循环3
线程二循环2
线程一循环4
线程一循环5
线程一循环6
线程一循环7
线程二循环3
线程一循环8
线程二循环4
线程一循环9
线程二循环5
线程二循环6
线程二循环7
线程二循环8
线程二循环9

结果说明:
“线程t1”在能被4整数的时候,并没有切换到“线程t2”。这表明,yield()虽然可以让线程由“运行状态”进入到“就绪状态”;但是,它不一定会让其它线程获取CPU执行权(即,其它线程进入到“运行状态”),即使这个“其它线程”与当前调用yield()的线程具有相同的优先级。

yield() 与 wait()的比较
(01) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而yield()是让线程由“运行状态”进入到“就绪状态”。
(02) wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。

public class YieldLockTest{ 

    private static Object obj = new Object();

    public static void main(String[] args){ 
        ThreadA t1 = new ThreadA("t1"); 
        ThreadA t2 = new ThreadA("t2"); 
        t1.start(); 
        t2.start();
    } 

    static class ThreadA extends Thread{
        public ThreadA(String name){ 
            super(name); 
        } 
        public void run(){ 
            // 获取obj对象的同步锁
            synchronized (obj) {
                for(int i=0; i <10; i++){
                    System.out.println(Thread.currentThread().getName() + "循环" + i);
                    // i整除4时,调用yield
                    if (i%4 == 0)
                        Thread.yield();
                }
            }
        } 
    } 
}

其中一次运行结果
t1循环0
t1循环1
t1循环2
t1循环3
t1循环4
t1循环5
t1循环6
t1循环7
t1循环8
t1循环9
t2循环0
t2循环1
t2循环2
t2循环3
t2循环4
t2循环5
t2循环6
t2循环7
t2循环8
t2循环9

结果说明:
主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(obj)。在t1运行过程中,虽然它会调用Thread.yield();但是,t2是不会获取cpu执行权的。因为,t1并没有释放“obj所持有的同步锁”!

7. 线程休眠

sleep()介绍
sleep() 定义在Thread.java中。它的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。

sleep()代码示例

public class MyThread implements Runnable {

    @Override
    public synchronized void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "循环打印" + i);
        }
    }
}

public class TestThread {
    public static void main(String[] args) {
        MyThread task = new MyThread();
        Thread t1 = new Thread(task, "t1");
        t1.start();
    }
}

运行结果:每隔一秒打印一行
t1循环打印0
t1循环打印1
t1循环打印2
t1循环打印3
t1循环打印4
t1循环打印5
t1循环打印6
t1循环打印7
t1循环打印8
t1循环打印9

sleep() 与 wait()的比较
wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”。但是,wait()会释放对象的同步锁,而sleep()则不会释放锁。
下面通过代码示例演示sleep()是不会释放锁的:

public class SleepLockTest{ 

    private static Object obj = new Object();

    public static void main(String[] args){ 
        ThreadA t1 = new ThreadA("t1"); 
        ThreadA t2 = new ThreadA("t2"); 
        t1.start(); 
        t2.start();
    } 

    static class ThreadA extends Thread{
        public ThreadA(String name){ 
            super(name); 
        } 
        public void run(){ 
            synchronized (obj) {
                try {
                    for(int i=0; i < 5; i++){ 
                        System.out.println(this.getName() + "循环打印" + i);
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } 
    } 
}

// 打印结果
t1循环打印0
t1循环打印1
t1循环打印2
t1循环打印3
t1循环打印4
t2循环打印0
t2循环打印1
t2循环打印2
t2循环打印3
t2循环打印4

结果说明:
主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(obj)。在t1运行过程中,虽然它会调用Thread.sleep(1000);但是,t2是不会获取cpu执行权的。因为,t1并没有释放“obj所持有的同步锁”!

8. join()

join()介绍
join() 定义在Thread.java中。
join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。我们通过例子去理解:

public class Father extends Thread {
    public void run() {
        Son son = new Son();
        son.start();
        try {
            son.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("儿子结束了我再执行");
    }
}

public class Son extends Thread{
    public void run(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我结束了老爸再执行");
    }
}

public class Demo {
    public static void main(String[] args) {
        Father father = new Father();
        Son son = new Son();
        father.start();
        // son.start();
    }
}

执行结果:
我结束了老爸再执行
儿子结束了我再执行

结果说明:
上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。在Father主线程中,通过new Son()新建“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。 这也就是我们所说的“join()的作用,是让主线程会等待子线程结束之后才能继续运行”!

join()源码分析

public final void join() throws InterruptedException {
        join(0);
    }

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

说明:
从代码中,我们可以发现。当millis==0时,会进入while(isAlive())循环;即只要子线程是活的,主线程就不停的等待。
问题:
虽然s.join()被调用的地方是发生在“Father主线程”中,但是s.join()是通过“子线程s”去调用的join()。那么,join()方法中的isAlive()应该是判断“子线程s”是不是Alive状态;对应的wait(0)也应该是“让子线程s”等待才对。但如果是这样的话,s.join()的作用怎么可能是“让主线程等待,直到子线程s完成为止”呢,应该是让"子线程等待才对(因为调用子线程对象s的wait方法嘛)"?
答案:wait()的作用是让“当前线程”等待,而这里的“当前线程”是指当前在CPU上运行的线程。所以,虽然是调用子线程的wait()方法,但是它是通过“主线程”去调用的;所以,休眠的是主线程,而不是“子线程”!

join()代码示例

public class JoinTest {

    public static void main(String[] args) {
        try {
            ThreadC t1 = new ThreadC("t1"); // 新建“线程t1”

            t1.start();                     // 启动“线程t1”
            t1.join();                        // 将“线程t1”加入到“主线程main”中,并且“主线程main()会等待它的完成”
            System.out.println(Thread.currentThread().getName() + "线程继续工作");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class ThreadC extends Thread {

        public ThreadC(String name) {
            super(name);
        }

        public void run() {
            System.out.println(this.getName() + "线程开始工作");

            // 延时操作
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println(this.getName() + "线程结束工作");
        }
    }
}

结果:
t1线程开始工作
t1线程结束工作
main线程继续工作

结果说明:
运行流程如图
(01) 在“主线程main”中通过 new ThreadC(“t1”) 新建“线程t1”。 接着,通过 t1.start() 启动“线程t1”,并执行t1.join()。
(02) 执行t1.join()之后,“主线程main”会进入“阻塞状态”等待t1运行结束。“子线程t1”结束之后,会唤醒“主线程main”,“主线程”重新获取cpu执行权,继续运行。
在这里插入图片描述

9. interrupt()和线程终止方式

interrupt()说明

Interrupts this thread.
Unless the current thread is interrupting itself, which is always permitted, the checkAccess method of this thread is invoked, which may cause a SecurityException to be thrown.
If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.
If this thread is blocked in an I/O operation upon an interruptible channel then the channel will be closed, the thread's interrupt status will be set, and the thread will receive a ClosedByInterruptException.
If this thread is blocked in a Selector then the thread's interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector's wakeup method were invoked.
If none of the previous conditions hold then this thread's interrupt status will be set.
Interrupting a thread that is not alive need not have any effect.

中文意思:
interrupt()的作用是中断本线程。
本线程中断自己是被允许的;其它线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限。这有可能抛出SecurityException异常。
如果本线程是处于阻塞状态:调用线程的wait(), wait(long)或wait(long, int)会让它进入等待(阻塞)状态,或者调用线程的join(), join(long), join(long, int), sleep(long), sleep(long, int)也会让它进入阻塞状态。若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。
如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时;线程的中断标记会被设置为true,并且它会立即从选择操作中返回。
如果不属于前面所说的情况,那么通过interrupt()中断线程时,它的中断标记会被设置为“true”。
中断一个“已终止的线程”不会产生任何操作。

终止线程的方式
Thread中的stop()和suspend()方法,由于固有的不安全性,已经建议不再使用,我先分别讨论线程在“阻塞状态”和“运行状态”的终止方式,然后再总结出一个通用的方式。

1、终止处于“阻塞状态”的线程
通常,我们通过“中断”方式终止处于“阻塞状态”的线程。当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。将InterruptedException放在适当的位置就能终止线程,形式如下:

@Override
public void run() {
    try {
        while (true) {
            // 执行任务...
        }
    } catch (InterruptedException ie) {  
        // 由于产生InterruptedException异常,退出while(true)循环,线程终止!
    }
}

说明:在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()会产生InterruptedException中断。中断的捕获在while(true)之外,这样就会退出while(true)循环!
注意:对InterruptedException的捕获一般放在while(true)循环体的外面,这样,在产生异常时就退出了while(true)循环。否则,InterruptedException在while(true)循环体之内,就需要额外的添加退出处理。形式如下:

@Override
public void run() {
    while (true) {
        try {
            // 执行任务...
        } catch (InterruptedException ie) {  
            // InterruptedException在while(true)循环体内。
            // 当线程产生了InterruptedException异常时,while(true)仍能继续运行!需要手动退出
            break;
        }
    }
}

2、终止处于“运行状态”的线程
通常,我们通过“标记”方式终止处于“运行状态”的线程。其中,包括“中断标记”和“额外添加标记”。
(01) 通过“中断标记”终止线程。形式如下:

@Override
public void run() {
    while (!isInterrupted()) {
        // 执行任务...
    }
}

说明:isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。
注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。

(02) 通过“额外添加标记”。形式如下:

private volatile boolean flag= true;

protected void stopTask() {
    flag = false;
}

@Override
public void run() {
    while (flag) {
        // 执行任务...
    }
}

说明:线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。
注意:将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。

综合线程处于“阻塞状态”和“运行状态”的终止方式,比较通用的终止线程的形式如下:

@Override
public void run() {
    try {
        // 1. isInterrupted()保证,只要中断标记为true就终止线程。
        while (!isInterrupted()) {
            // 执行任务...
        }
    } catch (InterruptedException ie) {  
        // 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
    }
}

终止线程的代码示例
interrupt()常常被用来终止“阻塞状态”线程。参考下面示例:

// Demo1.java的源码
class MyThread extends Thread {
    
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        try {  
            int i=0;
            while (!isInterrupted()) {
                Thread.sleep(100); // 休眠100ms
                i++;
                System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);  
            }
        } catch (InterruptedException e) {  
            System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");  
        }
    }
}

public class Demo1 {

    public static void main(String[] args) {  
        try {  
            Thread t1 = new MyThread("t1");  // 新建“线程t1”
            System.out.println(t1.getName() +" ("+t1.getState()+") is new.");  

            t1.start();                      // 启动“线程t1”
            System.out.println(t1.getName() +" ("+t1.getState()+") is started.");  

            // 主线程休眠300ms,然后主线程给t1发“中断”指令。
            Thread.sleep(300);
            t1.interrupt();
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");

            // 主线程休眠300ms,然后查看t1的状态。
            Thread.sleep(300);
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
        } catch (InterruptedException e) {  
            e.printStackTrace();
        }
    } 
}

// 运行结果
t1 (NEW) is new.
t1 (RUNNABLE) is started.
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
t1 (TIMED_WAITING) is interrupted.
t1 (RUNNABLE) catch InterruptedException.
t1 (TERMINATED) is interrupted now.

结果说明:
(01) 主线程main中通过new MyThread(“t1”)创建线程t1,之后通过t1.start()启动线程t1。
(02) t1启动之后,会不断的检查它的中断标记,如果中断标记为“false”;则休眠100ms。
(03) t1休眠之后,会切换到主线程main;主线程再次运行时。开始时 isInterrupted() 是false。执行t1.interrupt()中断线程t1 触发中断。isInterrupted() 是true。遇到阻塞又set为false,而且会抛出InterruptedException异常。在t1的run()方法中,是在循环体while之外捕获的异常;因此循环被终止。

我们对上面的结果进行小小的修改,将run()方法中捕获InterruptedException异常的代码块移到while循环体内。

// Demo2.java的源码
class MyThread extends Thread {
    
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        int i=0;
        while (!isInterrupted()) {
            try {
                Thread.sleep(100); // 休眠100ms
            } catch (InterruptedException ie) {  
                System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");  
            }
            i++;
            System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);  
        }
    }
}

public class Demo2 {

    public static void main(String[] args) {  
        try {  
            Thread t1 = new MyThread("t1");  // 新建“线程t1”
            System.out.println(t1.getName() +" ("+t1.getState()+") is new.");  

            t1.start();                      // 启动“线程t1”
            System.out.println(t1.getName() +" ("+t1.getState()+") is started.");  

            // 主线程休眠300ms,然后主线程给t1发“中断”指令。
            Thread.sleep(300);
            t1.interrupt();
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");

            // 主线程休眠300ms,然后查看t1的状态。
            Thread.sleep(300);
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
        } catch (InterruptedException e) {  
            e.printStackTrace();
        }
    } 
}

t1 (NEW) is new.
t1 (RUNNABLE) is started.
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
t1 (TIMED_WAITING) is interrupted.
t1 (RUNNABLE) catch InterruptedException.
t1 (RUNNABLE) loop 3
t1 (RUNNABLE) loop 4
t1 (RUNNABLE) loop 5
t1 (TIMED_WAITING) is interrupted now.
t1 (RUNNABLE) loop 6
t1 (RUNNABLE) loop 7
t1 (RUNNABLE) loop 8
t1 (RUNNABLE) loop 9
...

结果说明:
程序进入了死循环!
为什么会这样呢?这是因为,t1在“等待(阻塞)状态”时,被interrupt()中断;此时,会清除中断标记[即isInterrupted()会返回false],而且会抛出InterruptedException异常[该异常在while循环体内被捕获]。因此,t1理所当然的会进入死循环了。
解决该问题,需要我们在捕获异常时,额外的进行退出while循环的处理。例如,在MyThread的catch(InterruptedException)中添加break 或 return就能解决该问题。

下面是通过“额外添加标记”的方式终止“运行状态”的线程的示例:

// Demo3.java的源码
class MyThread extends Thread {

    private volatile boolean flag= true;
    public void stopTask() {
        flag = false;
    }
    
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        synchronized(this) {
            try {
                int i=0;
                while (flag) {
                    Thread.sleep(100); // 休眠100ms
                    i++;
                    System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);  
                }
            } catch (InterruptedException ie) {  
                System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");  
            }
        }  
    }
}

public class Demo3 {

    public static void main(String[] args) {  
        try {  
            MyThread t1 = new MyThread("t1");  // 新建“线程t1”
            System.out.println(t1.getName() +" ("+t1.getState()+") is new.");  

            t1.start();                      // 启动“线程t1”
            System.out.println(t1.getName() +" ("+t1.getState()+") is started.");  

            // 主线程休眠300ms,然后主线程给t1发“中断”指令。
            Thread.sleep(300);
            t1.stopTask();
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");

            // 主线程休眠300ms,然后查看t1的状态。
            Thread.sleep(300);
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
        } catch (InterruptedException e) {  
            e.printStackTrace();
        }
    } 
}

t1 (NEW) is new.
t1 (RUNNABLE) is started.
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
t1 (TIMED_WAITING) is interrupted.
t1 (RUNNABLE) loop 3
t1 (TERMINATED) is interrupted now.

interrupted() 和 isInterrupted()的区别
interrupted() 和 isInterrupted()都能够用于检测对象的“中断标记”。区别是 interrupted()除了返回中断标记之外,它还会清除中断标记(即将中断标记设为false);而isInterrupted()仅仅返回中断标记。

10. 线程优先级和守护线程

线程优先级的介绍
java 中的线程优先级的范围是1~10,默认的优先级是5。“高优先级线程”会优先于“低优先级线程”执行。
java 中有两种线程:用户线程和守护线程。可以通过isDaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。
用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。需要注意的是:Java虚拟机在“用户线程”都结束后会后退出。

JDK 中关于线程优先级和守护线程的介绍如下:

Every thread has a priority. Threads with higher priority are executed in preference to threads with lower priority. Each thread may or may not also be marked as a daemon. When code running in some thread creates a new Thread object, the new thread has its priority initially set equal to the priority of the creating thread, and is a daemon thread if and only if the creating thread is a daemon.

When a Java Virtual Machine starts up, there is usually a single non-daemon thread (which typically calls the method named main of some designated class). The Java Virtual Machine continues to execute threads until either of the following occurs:

The exit method of class Runtime has been called and the security manager has permitted the exit operation to take place.
All threads that are not daemon threads have died, either by returning from the call to the run method or by throwing an exception that propagates beyond the run method. 
Marks this thread as either a daemon thread or a user thread. The Java Virtual Machine exits when the only threads running are all daemon threads.

每个线程都有一个优先级。“高优先级线程”会优先于“低优先级线程”执行。每个线程都可以被标记为一个守护进程或非守护进程。在一些运行的主线程中创建新的子线程时,子线程的优先级被设置为等于“创建它的主线程的优先级”,当且仅当“创建它的主线程是守护线程”时“子线程才会是守护线程”。

当Java虚拟机启动时,通常有一个单一的非守护线程(该线程通过是通过main()方法启动)。JVM会一直运行直到下面的任意一个条件发生,JVM就会终止运行:
(01) 调用了exit()方法,并且exit()有权限被正常执行。
(02) 所有的“非守护线程”都死了(即JVM中仅仅只有“守护线程”)。

每一个线程都被标记为“守护线程”或“用户线程”。当只有守护线程运行时,JVM会自动退出。

线程优先级的代码示例

public class ThreadB extends Thread{

    public ThreadB(String name){
        super(name);
    }

    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 线程级别: " + Thread.currentThread().getPriority() + " 循环 " + i);
        }
    }
}

public class Demo2 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + " 线程级别: " + Thread.currentThread().getPriority());

        ThreadB t1 = new ThreadB("t1");
        ThreadB t2 = new ThreadB("t2");
        t1.setPriority(1);
        t2.setPriority(10);
        t1.start();
        t2.start();
    }
}

main 线程级别: 5
t2 线程级别: 10 循环 0
t1 线程级别: 1 循环 0
t2 线程级别: 10 循环 1
t1 线程级别: 1 循环 1
t2 线程级别: 10 循环 2
t2 线程级别: 10 循环 3
t2 线程级别: 10 循环 4
t1 线程级别: 1 循环 2
t1 线程级别: 1 循环 3
t1 线程级别: 1 循环 4

结果说明:
(01) 主线程main的优先级是5。
(02) t1的优先级被设为1,而t2的优先级被设为10。cpu在执行t1和t2的时候,根据时间片轮循调度,所以能够并发执行。

守护线程的代码示例

class MyThread extends Thread{  
    public MyThread(String name) {
        super(name);
    }

    public void run(){
        try {
            for (int i=0; i<5; i++) {
                Thread.sleep(3);
                System.out.println(this.getName() +"(isDaemon="+this.isDaemon()+ ")" +", loop "+i);
            }
        } catch (InterruptedException e) {
        }
    } 
}; 

class MyDaemon extends Thread{  
    public MyDaemon(String name) {
        super(name);
    }

    public void run(){
        try {
            for (int i=0; i<10000; i++) {
                Thread.sleep(1);
                System.out.println(this.getName() +"(isDaemon="+this.isDaemon()+ ")" +", loop "+i);
            }
        } catch (InterruptedException e) {
        }
    } 
}
public class Demo {  
    public static void main(String[] args) {  

        System.out.println(Thread.currentThread().getName()
                +"(isDaemon="+Thread.currentThread().isDaemon()+ ")");

        Thread t1=new MyThread("t1");    // 新建t1
        Thread t2=new MyDaemon("t2");    // 新建t2
        t2.setDaemon(true);                // 设置t2为守护线程
        t1.start();                        // 启动t1
        t2.start();                        // 启动t2
    }  
}

main(isDaemon=false)
t2(isDaemon=true), loop 0
t2(isDaemon=true), loop 1
t2(isDaemon=true), loop 2
t1(isDaemon=false), loop 0
t2(isDaemon=true), loop 3
t2(isDaemon=true), loop 4
t2(isDaemon=true), loop 5
t1(isDaemon=false), loop 1
t2(isDaemon=true), loop 6
t2(isDaemon=true), loop 7
t2(isDaemon=true), loop 8
t1(isDaemon=false), loop 2
t2(isDaemon=true), loop 9
t2(isDaemon=true), loop 10
t2(isDaemon=true), loop 11
t1(isDaemon=false), loop 3
t2(isDaemon=true), loop 12
t2(isDaemon=true), loop 13
t2(isDaemon=true), loop 14
t1(isDaemon=false), loop 4

结果说明:
(01) 主线程main是用户线程,它创建的子线程t1也是用户线程。
(02) t2是守护线程。在“主线程main”和“子线程t1”(它们都是用户线程)执行完毕,只剩t2这个守护线程的时候,JVM自动退出,t2的循环也就终止了。

总结:
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。

11. 生产消费者问题

生产/消费者模型
生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括“生产者”、“消费者”、“仓库”和“产品”。他们之间的关系如下:
(01) 生产者仅仅在仓储未满时候生产,仓满则停止生产。
(02) 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
(03) 当消费者发现仓储没产品可消费时候会通知生产者生产。
(04) 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。

生产/消费者实现代码示例

// 仓库
public class Depot {

    // 仓库容量
    private int capacity;
    // 仓库实际数量
    private int size;

    // 满参构造
    public Depot(int capacity) {
        this.capacity = capacity;
        this.size = 0;
    }

    // 生产者
    public synchronized void produce(int val) {
        try {
            // 想要生产的数量
            int left = val;
            while (left > 0) {
                // 表示仓库已满,等待消费
                while (size >= capacity) {
                    wait();
                }
                // 实际要生产的数量
                int inc = (size + left) > capacity ? (capacity - size) : left;
                // 生产之后实际库存
                size += inc;
                // 生产之后想要生产的数量还剩多少
                left -= inc;
                System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d\n", Thread.currentThread().getName(), val, left, inc, size);
                // 通知消费者可以了
                notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void consume(int val) {
        try {
            // 想要消费的数量
            int left = val;
            while (left > 0) {
                // 实际库存已经空了
                while (size <= 0) {
                    wait();
                }
                // 实际消费数量
                int dec = (size < left) ? size : left;
                // 消费之后剩余库存
                size -= dec;
                // 由于库存不足,想要消费的数量还剩多少
                left -= dec;
                System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d\n",
                        Thread.currentThread().getName(), val, left, dec, size);
                // 通知生产者可以生产了
                notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String toString() {
        return "capacity:" + capacity + ", actual size:" + size;
    }
}
public class Customer {

    private Depot depot;

    public Customer(Depot depot) {
        this.depot = depot;
    }
    // 新建一个线程消费产品
    public void consume(int val){
        new Thread(){
            public void run(){
                depot.consume(val);
            }
        }.start();
    }
}
public class Producer {

    private Depot depot;

    public Producer(Depot depot) {
        this.depot = depot;
    }
    // 新建一个线程生产产品
    public void produce(int val){
        new Thread(){
            public void run(){
                depot.produce(val);
            }
        }.start();
    }
}
public class Test01 {
    public static void main(String[] args) {

        Depot depot = new Depot(100);

        Producer producer = new Producer(depot);
        Customer customer = new Customer(depot);

        producer.produce(60);
        producer.produce(120);
        customer.consume(90);
        customer.consume(150);
        producer.produce(110);
    }
}

Thread-0 produce( 60) --> left=  0, inc= 60, size= 60
Thread-3 consume(150) <-- left= 90, dec= 60, size=  0
Thread-4 produce(110) --> left= 10, inc=100, size=100
Thread-2 consume( 90) <-- left=  0, dec= 90, size= 10
Thread-1 produce(120) --> left= 30, inc= 90, size=100
Thread-3 consume(150) <-- left=  0, dec= 90, size= 10
Thread-4 produce(110) --> left=  0, inc= 10, size= 20
Thread-1 produce(120) --> left=  0, inc= 30, size= 50

说明:
(01) Producer是“生产者”类,它与“仓库(depot)”关联。当调用“生产者”的produce()方法时,它会新建一个线程并向“仓库”中生产产品。
(02) Customer是“消费者”类,它与“仓库(depot)”关联。当调用“消费者”的consume()方法时,它会新建一个线程并消费“仓库”中的产品。
(03) Depot是“仓库”类,仓库中记录“仓库的容量(capacity)”以及“仓库中当前产品数目(size)”。
“仓库”类的生产方法produce()和消费方法consume()方法都是synchronized方法,进入synchronized方法体,意味着这个线程获取到了该“仓库”对象的同步锁。这也就是说,同一时间,生产者和消费者线程只能有一个能运行。通过同步锁,实现了对“残酷”的互斥访问。
对于生产方法produce()而言:当仓库满时,生产者线程等待,需要等待消费者消费产品之后,生产线程才能生产;生产者线程生产完产品之后,会通过notifyAll()唤醒同步锁上的所有线程,包括“消费者线程”,即我们所说的“通知消费者进行消费”。
对于消费方法consume()而言:当仓库为空时,消费者线程等待,需要等待生产者生产产品之后,消费者线程才能消费;消费者线程消费完产品之后,会通过notifyAll()唤醒同步锁上的所有线程,包括“生产者线程”,即我们所说的“通知生产者进行生产”。

发布了86 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43365369/article/details/95222078