Java-多线程

概述

  • 进程:计算机中的程序关于某数据集合的一次运行活动,是系统进行资源分配和调度的基本单位。
  • 线程:就是进程中的一个负责程序执行的控制单元(执行路径)。
  • 一个进程中可以有多个执行路径,称之为多线程。
  • 一个进程中至少要有一个线程
  • 开启多个线程是为了同时运行多部分代码
  • 每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务

利弊

  • 多线程操作,可以让多个任务并行执行,a任务等待硬盘响应时,让b任务去用cpu,所以,多线程不能提高单任务的运行速度,但是可以提高多个可以并发的任务速度。
  • 好处:解决多部分同时运行的问题
  • 弊端:线程过多会导致效率的降低

JVM多线程分析

  • JVM启动时就启动了多个线程,至少有两个可以分析出来
  • 1.执行main函数的线程
    • 该线程的任务代码都定义在main函数中
  • 2.负责垃圾回收的线程

创建线程

创建方式一

  • 继承 Thread 类
    • 1、定义一个类继承 Thread 类
    • 2、必须覆盖 Thread 类的 run 方法
    • 3、直接创建 Thread 的子类对象创建线程
    • 4、调用 start 方法开启线程并调用线程的任务 run 方法执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
创建线程的目的是开启一条执行路径,去运行指定的代码和其他代码同时运行
而运行的代码就是这个执行路径的任务

JVM创建的主线程的任务都定义在主函数中
而自定义的线程,他的任务在哪儿?

Thread类用于描述线程,线程是需要任务的,所以Thread类也对任务的描述
这个任务就通过Thread类中的run方法来体现
也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义就是线程要运行的任务代码
*/
public class Demo extends Thread{
private String name;
public Demo(String name){
this.name = name;
}
public void run(){
show();
}
public void show(){
for(int i=0; i<10; i++)
System.out.println(name+"...i:"+i);
}
}
public class ThreadDemo {
public static void main(String[] args){
Demo a = new Demo("小白");
Demo b = new Demo("black");
a.start();
b.start();
//a.run();
//b.run();
}
}
  • 在上述代码的结尾,调用run和start函数有什么区别?
  • 调用start函数是多线程的,调用run函数还是按顺序主线程进行的

线程名称

  • 想要展示出当前运行对象线程名称需要使用Thread类的currentThread方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Demo extends Thread{
private String name;
public Demo(String name){
super();
this.name = name;
//super(name);
}
public void run(){
show();
}
public void show(){
for(int i=0; i<10; i++)
System.out.print(name+"...i:"+i);
System.out.println("ThreadName"+Thread.currentThread().getName());
}
}
public class ThreadDemo {
public static void main(String[] args){
Demo a = new Demo("小白");
Demo b = new Demo("black");
a.start();
b.start();
System.out.println("here"+"..."+Thread.currentThread().getName());
// a.run();
// b.run();
}
}
  • 运行后可以试着4.5行注释掉,6行解除注释试一下,发现可以自己给线程名称赋值了

图解

  • 开启了多线程后,栈内存就和之前不一样了,以前面代码为示例
  • 如果在上文倒数第三行加上一句代码“System.out.println(4/0);”
  • 观察运行结果,发现main函数已经抛出异常终止了,其余两线程仍能正常进行

线程的状态

创建方式二

  • 实现Runnable接口
    • 1、定义类实现Runnable接口
    • 2、覆盖接口中的run方法,将编程的任务代码封装到run方法中
    • 3、通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递
    • 4、调用线程对象的start方法开启线程
  • 第三步的原因
  • 因为线程的任务都封装在Runnable接口子类对象的run方法中,所以要在线程对象创建时就必须明确要运行的任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Demo implements Runnable{
//准备扩展Demo类的功能,让其中的内容可以作为多线程执行
//通过接口的方式完成
private String name;
public void show(){
for(int i=0; i<10; i++)
System.out.println(Thread.currentThread().getName()+"..."+i);
}
@Override
public void run() {
show();
}
}
public class ThreadDemo {
public static void main(String[] args){
Demo a = new Demo();
Demo b = new Demo();
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start();
t2.start();
System.out.println("here"+"..."+Thread.currentThread().getName());
}
}

方式二细节

方式二优点

  • 实现Runnable接口的好处
    • 1、将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成了对象
    • 2、避免了Java单继承的局限性
  • 所以创建线程的第二种方式比较常用

卖票示例

  • 在火车站一共有100张票,4个窗口同时卖票,用多线程来描述
  • 运行图中左侧代码后发现,并不是想象中的卖100张,而是变成了4*100张,可以选择将ticket类中的num值变成静态变量,但这不是最好的选择。
  • 另外还有人说可以不在堆中创建那么多的对象,只创建一个对象,进行4次线程开启,这是不可取的,多次启动一个线程是非法的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 class Ticket implements Runnable{
private int num = 100;
public void run(){
while(true){
if(num>0)
System.out.println(num--);
}
}
}
class TicketDemo{
public static void main(String[] args){
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
  • 通过继承的方式创建没有达到我们的目的,于是就采用上述代码中接口的方式创建

线程安全隐患

  • 以车站卖票为示例,如果当车票只剩一张时,t1进入临时堵塞状态,有执行资格不具备执行权,推给t2执行,t2也临时堵塞一下,继续往后推,然后假设t3,t4也是临时堵塞,那他们现在都具备执行资格,没有执行权,然后t1卖出一张票,票数为0,但是t2.3.4都已经进入循环,可以卖票,于是分别卖出了第0.-1.-2张票。这对于铁路局来说是不允许的,有安全隐患。
  • 有人说为什么我运行很多次都没有出现这种错误那,因为这只是有一定可能发生的,为了看起来更清晰,可以人为的方式,使产生安全隐患概率增加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Ticket implements Runnable{
    private int num = 100;
    public void run(){
    while(true){
    if(num>0){
    try{
    Thread.sleep(10);
    }catch(InterruptedException){
    //解决异常代码
    }
    System.out.println(num--);
    }
    }
    }
    }
    }
  • 将ticket类改成这样,在运行主类,发现会出现之前所说的安全隐患

线程安全问题产生原因

  • 1、多个线程在操作共享数据
  • 2、操作共享数据的线程代码有多条
  • 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生

解决思路(同步)

  • 就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算的,必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算
  • 在Java中用同步代码块就可以解决这个问题
1
2
3
4
同步代码块格式:
synchronized(对象){
需要被同步的代码;
}
  • 所以更改代码后变为这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Ticket implements Runnable{
    private int num = 100;
    Object obj = new Object();
    public void run(){
    while(true){
    synchronized(obj){
    if(num>0){
    try{
    Thread.sleep(10);
    }catch(InterruptedException){
    //解决异常代码
    }
    System.out.println(num--);
    }
    }
    }
    }
    }
  • 这次再去运行一下发现不会有问题

  • 这很像锁,可以比喻成火车上的卫生间,一个人进去后,门锁上,门上有提示“有人”,别人就进不来,只有里面的人出去才可以。

  • 同步的好处:解决了线程的安全问题

  • 同步的弊端:相对降低了效率,因为同步外的线程都会判断同步锁

    同步前提

  • 同步的前提:同步中必须有多个线程并使用同一个锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        private int num = 100;
    public void run(){
    Object obj = new Object();
    //注意:上面这句代码的位置和之前不同
    while(true){
    synchronized(obj){
    //代码
    }
    }
    }
    }
  • 运行上面的代码后发现并没有解决安全问题,是因为这样写相当于有四个锁,把二三行代码换位置即可

同步函数

  • 需求:两个储户去银行存款,每次100,存三次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Bank {
private int sum;
//private Object obj = new Object();
public synchronized void add(int num) {
//synchronized(obj){
sum = sum + num;
try { //延时,增加出错可能性
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sum);
//}
}
}
public class Cus implements Runnable{
private String name;
public Bank b = new Bank();

@Override
public void run() {
for (int i=0; i<3; i++) {
b.add(100);
}
}
}
public class BankDemo {
public static void main(String[] args){
Cus a = new Cus();
Thread t1 = new Thread(a);
Thread t2 = new Thread(a);
t1.start();
t2.start();
}
}
  • “多个线程在操作共享数据(sum),操作共享数据代码有多条(不算trycatch两条)”这两点都符合,所以有线程安全隐患
  • 像上面这种情况,可以使用同步代码块,不过还有更简洁的方式就是在函数中加上同步关键字

验证同步函数锁

  • 同步函数锁固定是this,同步代码块锁是任意的对象
  • 开发中建议使用同步代码块
  • 下面是验证时的代码(以双线程卖票为示例)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    public class Ticket implements Runnable {
    private int num = 100;
    boolean flag = true;
    public void run() {
    if (flag) {
    while (true) {
    synchronized (this) {
    if (num > 0) {
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "..emmm.." + num--);
    }
    }
    }
    }
    else {
    while (true)
    show();
    }
    }

    public synchronized void show() {
    if (num > 0) {
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "......" + num--);
    }
    }
    }
    public class SynFunctionLockDemo {
    public static void main(String[] args){
    Ticket t = new Ticket();
    Thread t1 = new Thread(t);
    Thread t2 = new Thread(t);
    t1.start();
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    t.flag = false;
    t2.start();
    }
    }

验证静态同步函数锁

  • 静态的同步函数使用的锁是 该函数所属的字节码文件对象
  • 可以用getClass方法获取,也可以用 当前类名.class 表示
  • 验证的代码和上面的大同小异,将this改为this.getClass或者Ticket.class再试试

单例模式涉及的多线程问题

安全隐患分析

  • 会出现安全隐患的是懒汉式,也就是延迟加载单例模式
1
2
3
4
5
6
7
8
9
public class Single2 {
private static Single2 s = null;
private Single2(){}
public static Single2 getInstance(){
if(s==null)
s = new Single2();
return s;
}
}
  • 仔细想一下,他符合了会出现隐患的两个因素,线程0在进行(s==null)判断后,进入临时堵塞状态,然后线程1进行null的判断,进入临时堵塞状态。这时线程0创建了s对象,返回s,线程1又创建了一个对象,现在就有两个对象,不能保证唯一了,这就出现大问题了。

解决方法

  • 加同步代码块解决安全问题,外面再加一次判断是解决效率问题
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Single2 {
private static Single2 s = null;
private Single2(){}
public static Single2 getInstance(){
if(s==null){
synchronized(Single2.class){
if(s==null)
s = new Single2();
}
}
return s;
}
}

死锁

  • 常见情景之一:同步的嵌套
1
2


  • run函数里拿着obj的锁想进this的锁,show函数里拿着this锁想进obj

简单死锁代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class MyLock {
//写两个锁
public static final Object MyLockA = new Object();
public static final Object MyLockB = new Object();
}
import static bxdDay_13.MyLock.MyLockA;
import static bxdDay_13.MyLock.MyLockB;

public class DeadLock implements Runnable{
public boolean flag;
//有参构造函数,创建对象时附带flag状态
public DeadLock(boolean flag){
this.flag = flag;
}

@Override
public void run() {
if (flag){
synchronized (MyLockA){
System.out.println(Thread.currentThread().getName()+" "+"if LockA");
synchronized (MyLockB){
System.out.println(Thread.currentThread().getName()+" "+"if LockB");
}
}
}else {
synchronized (MyLockB){
System.out.println(Thread.currentThread().getName()+" "+"else LockB");
synchronized (MyLockA){
System.out.println(Thread.currentThread().getName()+" "+"else LockA");
}
}
}
}
}
public class DeadLockTest {
public static void main(String[] args){
DeadLock a = new DeadLock(true);
DeadLock b = new DeadLock(false);
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start();
t2.start();
}
}
  • 运行上面的代码,观察结果,线程1拿到了B锁,线程2拿到了A锁,他们都想继续,可是互不相让,死锁了

    1
    2
    Thread-1 else LockB
    Thread-0 if LockA
  • 如果试了多次都是没有成功死锁,那就在 if语句 及 else语句 后面各加一句 while(true)

线程间通信

  • 多个线程在处理资源,但是任务却不同
  • 同步前提:多个线程使用同一个锁
  • 接下来用一个例子演示一下
  • 需求:有一个资源库存放数据,一条线程在向里面输入,还有一条在向外输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//资源类
public class Resource {
public String name;
public String sex;
}
//输入类
public class Input implements Runnable {
Resource r;
public Input(Resource r){
this.r = r;
}
@Override
public void run() {
int x = 0;
while(true) {
synchronized (r) {
if (x == 0) {
r.name = "小白";
r.sex = "女女女";
} else {
r.name = "black";
r.sex = "malemale";
}
x = (++x) % 2;
}
}
}
}
//输出类
public class Output implements Runnable {
Resource r;
public Output(Resource r){
this.r = r;
}
@Override
public void run() {
while (true) {
synchronized (r) {
System.out.println("name:" + r.name + "..." + "sex:" + r.sex);
}
}
}
}
//测试类
public class Test {
public static void main(String[] args){
//创建资源
Resource r = new Resource();
//创建任务
Input in = new Input(r);
Output out = new Output(r);
//创建线程,执行路径
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
//开启线程
t1.start();
t2.start();
}
}
  • 上面的代码中输入设置的是输入两个人名,需要注意的地方是,输入和输出类中加的锁是一样的,都是r,这个在测试类中,创建in、out对象时,要给他们传一样的参数。否则同步锁无用!
  • 但是这样的代码运行后还是不太好,因为输出的都是大片相同的人名,想让他们输入一个就输出一个,就需要使用线程的等待唤醒机制了。

等待唤醒机制

  • 涉及方法
    • wait():让线程处于冻结状态,被wait的线程会被存储到线程池中
    • notify():唤醒线程池中的一个线程(任意)
    • nitifyAll():唤醒线程池中的所有线程
  • 这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法,必须要明确到底操作的是哪个锁上的线程

  • 观察api文档后,为什么操作线程的方法wait、notify、notifyAll定义在Object类中那?

  • 因为这些方法是监视器的方法,监视器其实就是锁,锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//资源类
public class Resource {
public String name;
public String sex;
public boolean flag = false;
}
//输入类
public class Input implements Runnable {
Resource r;
public Input(Resource r){
this.r = r;
}
@Override
public void run() {
int x = 0;
while(true) {
synchronized (r) {
if (r.flag) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (x == 0) {
r.name = "小白";
r.sex = "女女女";
} else {
r.name = "black";
r.sex = "malemale";
}
r.flag = true;
r.notify();
}
x = (++x) % 2;
}
}
}
//输出类
public class Output implements Runnable {
Resource r;
public Output(Resource r){
this.r = r;
}
@Override
public void run() {
while (true) {
synchronized (r) {
if (!r.flag) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("name:" + r.name + "..." + "sex:" + r.sex);
r.flag = false;
r.notify();
}
}
}
}
  • 思路:输入数据之前判断一下是否有数据,如果有数据,控制输入的线程等待,先让控制输出的线程输出,输出数据后,将flag更改为false,意味着资源中数据为空,可以继续输入,然后再将控制输入的线程唤醒,自己(控制输出的线程)等待。这样就可以实现输入一个数据再输出一个数据了。
  • 注意:输入输出的锁都是 r,等待唤醒中的线程池都是基于这个锁(也就是 r)

代码优化

  • 上面的代码虽然最后实现了功能,但是封装性差,我们可以把输入,输出的方法定义在资源类中,在输入输出类中调用输入输出的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    //资源类
    public class Resource {
    private String name;
    private String sex;
    private boolean flag = false;
    //输入方法
    public synchronized void set(String name,String sex){
    if (this.flag) {
    try {
    this.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    this.name = name;
    this.sex = sex;

    this.flag = true;
    this.notify();
    }
    //输出方法
    public synchronized void out(){
    if (!this.flag) {
    try {
    this.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    System.out.println("name:" + name + "..." + "sex:" + sex);
    this.flag = false;
    this.notify();
    }
    }
    //输入类
    public class Input implements Runnable {
    private Resource r;
    public Input(Resource r){
    this.r = r;
    }
    @Override
    public void run() {
    int x = 0;
    while(true) {
    if (x == 0) {
    r.set("小白","女女女" );
    } else {
    r.set("black", "malemale");
    }
    x = (++x) % 2;
    }
    }
    }
    //输出类
    public class Output implements Runnable {
    private Resource r;
    public Output(Resource r){
    this.r = r;
    }
    @Override
    public void run() {
    while (true) {
    r.out();
    }
    }
    }
  • 对共享资源修改的语句都写在了资源类中,可以给他们加同步锁,但是还是在方法中加上同步关键字看上去更简洁,这样他们加的锁还是同一个锁—> this 。

生产者消费者问题

  • 假设生产者生产产品,消费者消费产品,和上面进出资源的问题差不多

问题一

  • 这里直接开始4个线程的,两个生产者,两个消费者,运行一下,会发现有错误,比如一个生产者生产完一件产品,消费者连着两次消费这一件产品。这类问题产生的原因是什么那?
  • t0、t1是生产者 t2、t3是消费者,让我们按照代码的顺序走一遍。
  • t0进行判断,生产产品(1),t0等待,t1进行判断,t1等待。t2进行判断,消费产品(1),t2等待,t3进行判断,t3等待。唤醒t0,t0没有进行判断,生产产品(2),t0等待。然后需要唤醒一个线程,但是这时线程池中有三个在等待,如果唤醒的是t1,t1没有进行判断直接生产产品(3),t1等待,唤醒t2,t2消费了产品(3),如果再唤醒t3,t3没有进行判断,就会再消费产品(3)
  • 问题1产生原因:if判断标记,只有一次,会导致不该运行的线程运行了,出现了数据错误的情况
  • 解决方法:将资源类中的输入输出方法的 if 判断改为 while 循环,这样就不会唤醒后跳过判断了,不过这样又会产生一个新的问题

问题二

  • 问题2:4个线程都处在等待状态,没人去唤醒他们
  • 产生原因:notify只能唤醒一个线程,如果本方唤醒本方,无意义,而且while+notify会导致死锁
  • 解决办法:将资源类中的输入输出方法的唤醒改为全部唤醒(notifyAll)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    //这里就只贴上资源类的代码了,其他类代码的写法和上面差不多
    //还需要有生产者类,消费者类 实现Runnable接口,和Test类
    //资源类
    public class Resource {
    private String name;
    private int count = 1;
    private boolean flag = false;
    public synchronized void set(String name){
    while (this.flag) {
    try {
    this.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    this.name = name + count;
    count++;
    System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);

    this.flag = true;
    this.notifyAll();
    }
    public synchronized void out(){
    while (!this.flag) {
    try {
    this.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
    this.flag = false;
    this.notifyAll();
    }
    }

问题三

  • 解决了第二个问题后,已经不会报错了,但是还有些小弊端,使用notifyAll方法的时候会唤醒所有等待线程,但是如果唤醒了本方等待线程会降低效率,能不能只唤醒对方线程那?
  • 解决方法:使用jdk1.5新特性中的Lock Condition,可以创建多个监视器,该问题中,创建两个即可。关于Lock,Condition可以看该文章偏靠后的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//资源类
public class Resource {
private String name;
private int count = 1;
private boolean flag = false;
public Lock lock = new ReentrantLock();
//通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者
public Condition producer_con = lock.newCondition();
public Condition consumer_con = lock.newCondition();

public void set(String name){ //生产者 t0 t1
lock.lock();
try {
while (flag) {
try {
//该生产者线程进入等待状态
producer_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);

flag = true;
//唤醒随机一个消费者
consumer_con.signal();
}finally { //如果抛出异常,必须将关锁写在finally中
lock.unlock();
}

}
public void out(){ //消费者 t2 t3
lock.lock();
try {
while (!flag) {
try {
//该消费者进入等待状态
consumer_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);

flag = false;
//唤醒随机一个生产者
producer_con.signal();
}finally {
lock.unlock();
}

}
}

locks包

Lock接口

  • java.util.concurrent.locks.Lock
  • Lock替代了同步代码块或者同步函数,将同步的隐式锁操作变成显式锁操作,同时更为灵活,可以一个锁加上多个监视器。
  • 方法:
    • lock():获取锁
    • unlock():释放锁,通常需要定义在finally代码块中
1
2
3
4
5
6
7
//同步代码块对于锁的操作是隐式的
Object obj = new Object();
void show(){
synchronized(obj){
...code...
}
}
1
2
3
4
5
6
7
8
//同步和锁被封装成了对象,并将操作锁的隐式方式定义到了该对象中
//将隐式动作变为了显式动作。
Lock lock = new ReentrantLock();
void show(){
lock.lock();//获取锁
...code...
lock.unlock();//释放锁
}
  • 如果上面代码第六行位置会抛异常的话,一定要将释放锁写在finally里
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Lock lock = new ReentrantLock();
    void show(){
    lock.lock();//获取锁
    try{
    ...code...
    }finally{
    lock.unlock();//释放锁
    }
    }

Condition接口

  • Condition接口出现替代Object中的wait notify notifyAll等方法,将他们单独进行了封装,变成了Condition监视器对象,可以与任意锁进行组合
  • 方法:
    • await():等待
    • signal():唤醒
    • signalAll():全部唤醒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//旧版功能演示
//在没有Lock,Condition之前,生产消费者问题只能创建一个监视器
Object obj = new Object();
synchronzied(obj){
obj.wait();
obj.notify();
obj.notifyAll();
}
//新版功能演示
Lock lock = new ReectrantLock();
//生产者的锁
Condition producter_con = lock.newCondition();
//消费者的锁
Condition consumer_con = lock.newCondition();
producter_con.await();
producter_con.signal();
producter_con.signalAll();

范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class BoundedBuffer {
final Lock lock = new ReentrantLock();//锁对象
final Condition notFull =lock.newCondition();//写线程条件 
final Condition notEmpty = lock.newCondition();//读线程条件 

final Object[] items = new Object[100];//缓存队列
//下面是操作上面数组的变量(前两个指针)
int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;

public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)//如果队列满了 
notFull.await();//阻塞写线程
items[putptr] = x;//赋值 
if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0
++count;//个数++
notEmpty.signal();//唤醒读线程
} finally {
lock.unlock();
}
}

public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)//如果队列为空
notEmpty.await();//阻塞读线程
Object x = items[takeptr];//取值 
if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0
--count;//个数--
notFull.signal();//唤醒写线程
return x;
} finally {
lock.unlock();
}
}
}
  • 这是一个处于多线程工作环境下的缓存区,缓存区提供了两个方法,put和take,put是存数据,take是取数据,内部有个缓存队列,具体变量和方法说明见代码,这个缓存区类实现的功能:有多个线程往里面存数据和从里面取数据,其缓存队列(先进先出后进后出)能缓存的最大数值是100,多个线程间是互斥的,当缓存队列中存储的值达到100时,将写线程阻塞,并唤醒读线程,当缓存队列中存储的值为0时,将读线程阻塞,并唤醒写线程,这也是ArrayBlockingQueue的内部实现。
  • 下面分析一下代码的执行过程:
    • 1、一个写线程执行,调用put方法;
    • 2、判断count是否为100,显然没有100;
    • 3、继续执行,存入值;
    • 4、判断当前写入的索引位置++后,是否和100相等,相等将写入索引值变为0,并将count+1;
    • 5、仅唤醒读线程阻塞队列中的一个;
    • 6、一个读线程执行,调用take方法;
    • 7、……
    • 8、仅唤醒写线程阻塞队列中的一个
  • 如果不用多个 Condition ,只有一个Condition或者使用的同步代码块的内容时,当存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。

线程终止

wait和sleep区别

  • wait可指定也可不指定时间,sleep必须指定时间
  • 同步中时,对cpu的执行权和锁的处理不同
    • wait:释放执行权,释放锁
    • sleep:释放执行权,不释放锁

终止线程方式

  • 停止线程:
    • stop方法:不推荐使用
    • run方法结束
  • 怎么控制线程中的任务结束那?
  • 任务中都会有循环结构,只要控制住循环就可以结束任务,控制循环通常就用定义标记来完成
1
2
3
4
5
6
7
8
9
10
11
12
//通过在主函数中调用setFlag方法,即可终止线程
public class StopThread implements Runnable{
private boolean flag = true;
public void run(){
while(flag){
System.out.println(Thread.currentThread().getName()+" run");
}
}
public void setFlag(){
flag = false;
}
}
  • 但是使用这种方法也有一定的弊端,如果线程处于冻结状态无法读取标记,程序就不能停下来了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //这样的程序无法终止
    public synchronized void run(){
    try {
    wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    while(flag){
    System.out.println(Thread.currentThread().getName()+" run");
    }
    }
  • 可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备cpu的执行资格,但是强制动作会发生InterruptedException异常,记得要处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class StopThread implements Runnable{
private boolean flag = true;
public synchronized void run(){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
//也可以选择在这里更改标记
//flag = false;
}
while(flag){
System.out.println(Thread.currentThread().getName()+" run");
}
}
public void setFlag(){
flag = false;
}
}

public class StopThreadDemo {
public static void main(String[] args){
StopThread s = new StopThread();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
int num = 1;
for(;;){
if(++num == 50){
//这里加上了interrupt方法
t1.interrupt();
t2.interrupt();
s.setFlag();
break;
}
System.out.println(Thread.currentThread().getName());
}
}
}

守护线程

  • setDaemon可以将线程变为守护线程(后台线程)
  • 后台线程在运行时与前台线程无差别,前台线程终止时必须手动终止,如果前台线程都终止了,后台线程自动终止

    线程小方法

    join

  • join方法也会抛出中断异常
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //有线程t0、t1
    t0.start();
    t0.join();
    t1.start();
    System.out.println(" ");
    //运行结果一定是t0线程运行完,
    //运行main线程,再运行t1线程

    //情况2
    t0.start();
    t1.start();
    t0.join();
    System.out.println(" ");
    //运行结果:t0,t1互相抢占cpu,main等t0执行完再执行

线程优先值

  • 可以给线程分配不同优先级的值,会让他们在执行中被执行几率增加
1
2
3
4
5
6
//t0为线程名
t0.setPriority(Thread.MAX_PROIORITY);
//为了方便,设置三个final值
//MAX_PROIORITY = 10
//MIN_PROIORITY = 0;
//NORM_PROIORITY = 5;

两道多线程面试题

  • 判断下面的程序是否能成功运行,如果不能,报错在哪一行
1
2
3
4
5
6
public class Test implements Runnable{
public void run(Thread t){}
}
//最后报错在第一行,第二行是子类的特有方法
//在这个接口的实现类中没有覆盖run方法
//报错解决:使用abstract修饰类或者覆盖run方法
  • 判断下面程序,运行结果是什么?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ThreadTest {
    public static void main(String[] main){
    new Thread(new Runnable() {
    public void run() {
    System.out.println("我是任务里面的 ");
    }
    }){
    public void run() {
    System.out.println("我是子类里面的 ");
    }
    }.start();
    }
    }
    //现在的运行结果是“我是子类里面的”
    //如果注释掉8.9.10行,结果是“我是任务里面的”