多线程学习记录
周瑜 Lv2

多线程学习记录

1.线程,进程,多线程

进程:

  • 说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
  • 而进程则是执行程序的一次执行过程,他是一个动态的概念,是系统资源分配的单位
  • 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程是CPU调度和执行的单位
  • 注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器,如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

线程:

  • 线程就算独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程
  • main()称之为主线程,为系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

2.线程创建的三种方式

  1. 继承Thread类

    • 继承Thread类,重写run()方法,调用start开启线程
    • 线程开启不一定立即执行,由CPU调度执行
  • 不建议使用:避免OOP单继承局限性

    • 启动线程:子类对象.start()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
         package demo4;
      //继承Thread类
      public class Demo1 extends Thread{
      //重写run方法
      @Override
      public void run() {
      for (int i = 0; i <200; i++) {
      System.out.println("我在玩手机"+i);
      }
      }

      public static void main(String[] args) {
      //创建一个线程对象
      Demo1 demo1 = new Demo1();
      //调用start()方法开启线程
      demo1.start();
      for (int i = 0; i < 2000; i++) {
      System.out.println("我在玩电脑"+i);
      }
      }
      }

      image-20210402115339652

使用多线程下载图片

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
package demo4;
//导入commons-io的jar包
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
//继承Thread的类,实现多线程
public class Demo2 extends Thread{

private String url; //图片路径
private String name; //图片保存的名称

public Demo2(String url, String name) {
this.url = url;
this.name = name;
}
//下载文件的执行体
@Override
public void run() {
WebDownLoader webDownLoader = new WebDownLoader();
webDownLoader.downLoad(url,name);
System.out.println("已下载:"+name);
}

public static void main(String[] args) {
//创建三个线程,同时下载3张不同的图片
Demo2 t1 = new Demo2("http://p3.pstatp.com/large/pgc-image/94ccea1ff7cd4c14803b0103af802611","1.jpg");
Demo2 t2 = new Demo2("http://p1.pstatp.com/large/pgc-image/3157fa07b87a43b9a915a444d607f998","2.jpg");
Demo2 t3 = new Demo2("http://p1.pstatp.com/large/pgc-image/01220b1dc5d448829d36418357dd9e76","3.jpg");
t1.start();
t2.start();
t3.start();
}
//下载器
class WebDownLoader{
public void downLoad(String url,String name){
try {
//下载方法
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常");
}
}
}
}

image-20210402141727382

image-20210402141753055

ps:下载图片的顺序并不是按照从上往下执行的,线程开启不一定立即执行,而是由CPU调度执行

  1. 实现Runnable接口

    • 启动线程:传入目标对象+Thread对象.start()
    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package demo4;
    //实现runnable接口开启多线程
    public class Demo3 implements Runnable{
    @Override
    public void run() {
    for (int i = 0; i < 200; i++) {
    System.out.println("我在学Nginx"+i);
    }
    }

    public static void main(String[] args) {
    //步骤跟继承Thread对象一样,只是启动线程不一样:传入目标对象+Thread对象.start()
    Demo3 demo3 = new Demo3();
    new Thread(demo3).start();
    for (int i = 0; i < 1000; i++) {
    System.out.println("我在学多线程"+i);
    }

    }
    }

    image-20210408102629033

  2. 实现Callable接口

    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
    package demo4;

    import org.apache.commons.io.FileUtils;

    import java.io.File;
    import java.io.IOException;
    import java.net.URL;
    import java.util.concurrent.*;

    /**
    * 创建线程方式3:实现Callable接口
    */
    public class Demo6 implements Callable<Boolean> {
    private String url; //图片路径
    private String name; //图片保存的名称

    public Demo6(String url, String name) {
    this.url = url;
    this.name = name;
    }

    @Override
    public Boolean call() throws Exception {
    //下载文件的执行体
    WebDownLoader webDownLoader = new WebDownLoader();
    webDownLoader.downLoad(url,name);
    System.out.println("已下载:"+name);
    return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    //创建三个线程,同时下载3张不同的图片
    Demo6 t1 = new Demo6("http://p3.pstatp.com/large/pgc-image/94ccea1ff7cd4c14803b0103af802611","1.jpg");
    Demo6 t2 = new Demo6("http://p1.pstatp.com/large/pgc-image/3157fa07b87a43b9a915a444d607f998","2.jpg");
    Demo6 t3 = new Demo6("http://p1.pstatp.com/large/pgc-image/01220b1dc5d448829d36418357dd9e76","3.jpg");
    //创建执行服务
    ExecutorService ser = Executors.newFixedThreadPool(3);
    //提交执行
    Future<Boolean> r1 = ser.submit(t1);
    Future<Boolean> r2 = ser.submit(t2);
    Future<Boolean> r3 = ser.submit(t3);

    //获取结果
    boolean rs1 = r1.get();
    boolean rs2= r2.get();
    boolean rs3 = r3.get();
    //关闭服务
    ser.shutdownNow();
    }
    //下载器
    class WebDownLoader{
    public void downLoad(String url,String name){
    try {
    //下载方法
    FileUtils.copyURLToFile(new URL(url),new File(name));
    } catch (IOException e) {
    e.printStackTrace();
    System.out.println("IO异常");
    }
    }
    }
    }

image-20210408123856986

3.初识并发问题

  • 多个线程操作同一个资源的情况下,线程就不安全了,造成数据紊乱

模拟抢票:

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
package demo4;

/**
* 模拟抢票
*/
public class Demo4 implements Runnable{
private int tick = 20;
@Override
public void run() {
while (true){
if (tick<=0){
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了第"+tick--+"张票");
}
}

public static void main(String[] args) {
Demo4 demo4 = new Demo4();
//开启三个线程同时抢票
new Thread(demo4,"张三").start();
new Thread(demo4,"小明").start();
new Thread(demo4,"黄牛").start();

}
}

image-20210408104406202

ps:我们可以看到这个票已经完全混乱了,出现了两个第14张票,按道理来说只能一个人拿到这张票。

模拟龟兔赛跑:

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
package demo4;

import java.security.PrivateKey;

/**
* 模拟龟兔赛跑
*/
public class Demo5 implements Runnable {
private static int total=1000; //比赛米数
private static int rabbit =10; //兔子的速度
private int rabbitDistance =0; //兔子跑的路程
private static int tortoise = 4; //乌龟的速度
private int tortoiseDistance =0; //乌龟跑的路程
private static String winner; //胜利者
boolean flag = true;
@Override
public void run() {
String name = Thread.currentThread().getName();
while (flag){
if(name.equals("兔子")){
//兔子跑的距离
rabbitDistance = rabbit+rabbitDistance;
System.out.println(name+"已经跑了"+rabbitDistance+"米");
//兔子看乌龟跑的慢,就掉以轻心,使用线程休眠模拟兔子睡觉
if(rabbitDistance==800){
System.out.println(name+":我已经跑了"+rabbitDistance+"米了,好累啊,乌龟还在后面,我先睡一会,呼呼呼~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//判断;如果兔子冲线胜利者就是兔子
if(rabbitDistance==total){
winner="兔子";
System.out.println("胜利者是"+winner);
flag=false;
break;
}
if(name.equals("乌龟")){
//乌龟跑的距离
tortoiseDistance = tortoise+tortoiseDistance;
System.out.println("乌龟已经跑了"+tortoiseDistance+"米");
//判断;如果乌龟冲线胜利者就是乌龟
if(tortoiseDistance>=total){
winner="乌龟";
System.out.println("胜利者是"+winner);
flag=false;
break;
}
}

}
}
public static void main(String[] args) {
Demo5 demo5 = new Demo5();
new Thread(demo5,"兔子").start();
new Thread(demo5,"乌龟").start();
}

}

image-20210408115119281

image-20210408115131223

ps:我们可以看到兔子跑到800米的时候就掉以轻心,开始睡觉了,而乌龟坚持不懈,最终取得胜利。

4.Lambda表达式

Java8的新特性其实实质还是属于函数式编程的概念

函数式接口的定义:

  • 任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口

    1
    2
    3
    public interface Runnable{
    public abstract void run();
    }
  • 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象

作用:

  • 避免匿名内部类定义过多
  • 可以让你的代码看起来很简洁
  • 去掉了一堆没有意义的代码,只留下了核心的逻辑

语法:

1
2
3
4
5
6
//params:参数  expression:表达式  statement:语句
(params) -> expression
(params) -> statement
(params) -> {statements}
//示例:
a-> System.out.println("i like lambda-->"+a)

lambda表达式演变过程:

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
package demo4;

/**
* Lambda表达式的演变过程
*/
public class Demo7 {
//3.静态内部类
static class Like implements Ilike{
@Override
public void lambda() {
System.out.println("我喜欢静态内部类");
}
}

public static void main(String[] args) {
//调用函数接口类的方法
Ilike ilike = new IlikeImpl();
ilike.lambda();
//调用静态内部类的方法
ilike = new Like();
ilike.lambda();

//4.局部内部类
class Like1 implements Ilike{
@Override
public void lambda() {
System.out.println("我喜欢局部内部类");
}
}
//调用局部内部类的方法
ilike = new Like1();
ilike.lambda();

//5.匿名内部类,没有内的名称,必须借助接口或者父类
ilike = new Ilike() {
@Override
public void lambda() {
System.out.println("我喜欢匿名内部类");
}
};
ilike.lambda();
//6.lambda表达式
ilike = ()-> {
System.out.println("我喜欢lambda表达式");
};
ilike.lambda();
}
}
//1.函数接口类
interface Ilike{
void lambda();
}
//2.实现类
class IlikeImpl implements Ilike{

@Override
public void lambda() {
System.out.println("我喜欢函数接口类");
}
}

lambda简化注意点:

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
package demo4;

/**
* lambda简化
*/
public class Demo8 {

public static void main(String[] args) {
Hobby hobby = new HobbyImpl();
//lambda表达式
hobby = (a)->{
System.out.println("我喜欢"+a);
};
//简化括号的lambda表达式
hobby = a -> {
System.out.println("我喜欢"+a);
};
//简化大括号的lambda表达式
hobby = a -> System.out.println("我喜欢"+a);
hobby.like("你");
}
}

interface Hobby{
void like(String a);
}
class HobbyImpl implements Hobby{

@Override
public void like(String a) {
System.out.println("我喜欢"+a);
}
}

总结:

  1. lambda表达式只能有一行代码的情况下才能省略大括号,如果有多行就需要用大括号包裹
  2. lambda表达式使用的前提是接口为函数式接口
  3. 多个参数也可以去掉参数类型,要去掉就都去掉,必须加上括号

5.线程状态

线程的五大状态:

image-20210408174122109

image-20210408174632188

线程方法:

  • setPriority(int newPriority):更改线程的优先级
  • static void sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠
  • void join() :等待该线程终止
  • static void yield():暂停当前正在执行的线程对象,并执行其他线程
  • void interrupt():中断线程,别用这个方式
  • boolean isAlive():测试线程是否处于活动状态

停止线程:

  • 不推荐使用JDK提供的stop()、destroy()方法。【已废弃】
  • 推荐线程自动停止下来
  • 建议使用一个标志位进行终止变量,当flag=false,则终止线程运行。
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
package demo4;

/**
* 线程停止
* 1.建议线程正常停止--->利用次数,不建议死循环
* 2.建议使用标志位--->设置一个标志位
* 3.不要使用stop或者destroy等过时或者JDK不建议使用的方法
*/
public class Demo9 implements Runnable {
//1.设置一个标志位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
System.out.println("线程正在运行"+i++);
}
}
//2.设置一个公开的方法停止线程,转换标志位
public void stop(){
this.flag=false;
}

public static void main(String[] args) {
Demo9 demo9 = new Demo9();
new Thread(demo9).start();
for (int i = 1; i <=1000; i++) {
System.out.println("main"+i);
if(i==900){
//调用stop方法切换标志位,让线程停止
demo9.stop();
System.out.println("线程该停止了");
}
}
}
}

image-20210408182150608

线程休眠:

  • sleep(时间)指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException;
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁
  • 通常用来模拟网络延时:放大问题的发生性

休眠实例

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
package demo5;

/**
* 模拟倒计时
*/
public class Demo1 implements Runnable {
int s = 10;

@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(s--);
if(s==0){
break;
}
}
}

public static void main(String[] args) {
Demo1 demo1 = new Demo1();
new Thread(demo1).start();
}
}
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
package demo5;

/**
* 获取当前时间
*/

import java.text.SimpleDateFormat;
import java.util.Date;

public class Demo2 implements Runnable {
//获取系统当前时间
Date now = new Date(System.currentTimeMillis());
@Override
public void run() {
//打印当前时间
System.out.println(new SimpleDateFormat("HH:mm:ss").format(now));
//更新当前时间
now = new Date(System.currentTimeMillis());
//获取时间后休眠一秒,就算隔一秒打印一次
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
Demo2 demo2 = new Demo2();
new Thread(demo2).start();
}
}

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看CPU心情

礼让实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package demo5;

/**
* 线程礼让
*/
public class Demo3 {
public static void main(String[] args) {
TestYield testYield = new TestYield();
new Thread(testYield,"a").start();
new Thread(testYield,"b").start();
}
}

class TestYield implements Runnable{

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程正在执行");
//线程礼让
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程停止执行");
}
}

image-20210409095037365

ps:礼让不一定成功!看CPU心情

join合并线程

  • Join合并线程,待此线程执行完成后,在执行其他线程,其他线程阻塞
  • 可以想象成插队

Join实例

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
package demo5;

/**
* join
*/
public class Demo4 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 200; i++) {
System.out.println("vip线程插队,我先执行"+i);
}
}

public static void main(String[] args) throws InterruptedException {
Demo4 demo4 = new Demo4();
Thread thread = new Thread(demo4);
thread.start();
for (int i = 1; i <= 500; i++) {
if (i==200){
thread.join();
}
System.out.println("main线程正在执行"+i);
}

}
}

image-20210409101408314

ps:当我们main线程执行到200的时候,我们的vip线程就使用join线程插队,等vip线程执行完,main线程在执行

观测线程状态

线程状态,线程可以处于以下状态之一:

  • NEW –尚未启动的线程处于此状态
  • RUNNABLE –在Java虚拟机中执行的线程处于此状态
  • BLOCKED –被阻塞等待监视器锁定的线程处于此状态
  • WAITING –正在等待另一个线程执行特定动作的线程处于此状态
  • TIMED_WAITING –正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
  • TERMINATEO –已退出的线程处于此状态

一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态

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
package demo5;

/**
* 观测线程状态
*/
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("//////////");
});
//观察状态
Thread.State state = thread.getState();
System.out.println(state); //NEW
//启动线程
thread.start();
//更新线程状态
state=thread.getState(); //RUNNABLE
System.out.println(state);

//当线程还没死亡时,每隔100毫秒打印状态
while (state!=Thread.State.TERMINATED){
Thread.sleep(1000);
state = thread.getState();
System.out.println(state);
}

}
}

image-20210409104927120

线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行

  • 线程的优先级用数字表示,范围从1~10

    • Thread.MIN_PRIORITY=1; –最小优先级
    • Thread.MAX_PRIORITY=10; –最大优先级
    • Thread.NORM_PRIORITY=5; –默认优先级
  • 使用以下方式改变或获取优先级

    • getPriority().setPriority(int xxx)
  • 优先级的设定建议在start()调度前

  • 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看cpu的调度

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
package demo5;

/**
* 线程优先级
*/
public class Demo6 {
public static void main(String[] args) {
//主线程是默认优先级的
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
Prioirty prioirty = new Prioirty();
Thread thread = new Thread(prioirty);
Thread thread1 = new Thread(prioirty);
Thread thread2 = new Thread(prioirty);
Thread thread3 = new Thread(prioirty);
Thread thread4 = new Thread(prioirty);

//先设置优先级在启动
thread.setPriority(5);
thread.start();
thread1.setPriority(Thread.MAX_PRIORITY);
thread1.start();
thread2.setPriority(Thread.MIN_PRIORITY);
thread2.start();
thread3.setPriority(3);
thread3.start();
thread4.setPriority(2);
thread4.start();
}
}
class Prioirty implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
}
}

image-20210409121831309

image-20210409121922577

ps:我的优先级好像不起作用,每次运行结果都不一样

守护线程

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 守护线程如:后台记录操作日志,监控内存,垃圾回收等待…
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
package demo5;

/**
* 测试守护线程
*/
public class Demo7 {
public static void main(String[] args) {
God god = new God();
Person person = new Person();
Thread thread = new Thread(god);
thread.setDaemon(true); //默认是false表示是用户线程,正常的线程都是用户线程
thread.start();
new Thread(person).start(); //用户线程启动
}
}

class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("上帝守护着人们!");
}
}
}
class Person implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 36500; i++) {
System.out.println("人们已经活了"+i+"天");
}
System.out.println("=====Good Bye World======");
}
}

image-20210409143815417

image-20210409143828016

ps:当我们的用户线程执行完,守护线程也会自动关闭,为什么守护线程没有立刻关闭呢?

  • 因为我们的虚拟机关闭也是要是时间的,这就看出了我们的虚拟机不用等待守护线程执行结束

线程同步机制

并发:同一个对象被多个线程同时操作 如:上万人同时抢100张票,两个银行同时取钱

  • 现实生活中,我们会遇到同一个资源,多个人都想使用的问题,比如食堂排队打饭,每个人都想吃饭,最天然的解决办法就算排队,一个个来。
  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用
  • 由于同一线程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

几个线程不安全的示例

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
package demo5;

/**
* 不安全的买票
*/
public class Demo8 {
public static void main(String[] args) {
Buy buy = new Buy();
new Thread(buy,"张三").start();
new Thread(buy,"黄牛").start();
}
}

class Buy implements Runnable{
private int tick = 1;
@Override
public void run() {
if(tick<=0){
System.out.println("票已卖完");
return;
}
System.out.println(Thread.currentThread().getName()+"买到了第"+tick--+"张票");

}
}

image-20210409165459323

ps:两个人抢一张票,明明票被张三抢完了,而黄牛还会去抢

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
package demo5;

/**
* 不安全的取钱
*/
public class Demo9 {
public static void main(String[] args) {
Account account = new Account(100,"银行卡");
Drawing drawing = new Drawing(account,50,"张三");
Drawing drawing1 = new Drawing(account,100,"李四");
drawing.start();
drawing1.start();
}
}

class Account{
int money; //余额
String name; //卡名

public Account(int money, String name) {
this.money = money;
this.name = name;
}
}

class Drawing extends Thread{
Account account; //账户
int drawingMoney; //取了多少钱
int nowMoney; //现在有多少钱

public Drawing(Account account,int drawingMoney,String name){
super(name);
this.account=account;
this.drawingMoney=drawingMoney;
}

@Override
public void run() {
//判断有没有钱
if(account.money-drawingMoney<0){
System.out.println("余额不足!");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money=account.money-drawingMoney; //银行卡余额
nowMoney+=drawingMoney; //现在手里有的钱
System.out.println(this.getName()+"取了"+drawingMoney+"钱");
System.out.println(this.getName()+"现在手里有"+nowMoney+"钱");
System.out.println(account.name+"余额为"+account.money);
}
}

image-20210409180325685

ps:我们可以看到张三取了50,李四取了100,而银行卡里面只有100,这样取钱银行还不得破产

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package demo6;
/**
* 不安全的集合
*/
import java.util.ArrayList;
import java.util.List;

public class Demo1 {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(5000);
System.out.println(list.size());
}
}

image-20210410111611301

ps:我们可以看到打印不了10000条数据,可能是线程不安全抢了同一个下标

同步方法

  • 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就算synchronized关键字,它包括两种用法:synchronized方法和synchronized块。
  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
  • 缺陷:若将一个大的方法申明为synchronized将会影响效率

不安全的买票改造后

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
package demo5;

/**
* 不安全的买票
* 使用同步方法使其变成安全的
*/
public class Demo8 {
public static void main(String[] args) {
Buy buy = new Buy();
new Thread(buy,"张三").start();
new Thread(buy,"黄牛").start();
}
}

class Buy implements Runnable{
private int tick = 1;
//在方法上加入synchronized关键字
@Override
public synchronized void run() {
if(tick<=0){
System.out.println("票已卖完");
return;
}
System.out.println(Thread.currentThread().getName()+"买到了第"+tick--+"张票");

}
}

image-20210410105509541

同步块

  • 同步块:synchronized(obj){}

  • Obj称之为同步监视器

    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
  • 同步监视器的执行过程

    1. 第一个线程访问,锁定同步监视器,执行其中代码
    2. 第二个线程访问,发现同步监视器被锁定,无法访问
    3. 第一个线程访问完毕,解锁同步监视器
    4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

使用同步块改造不安全的取钱

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
package demo5;

/**
* 不安全的取钱
*/
public class Demo9 {
public static void main(String[] args) {
Account account = new Account(100,"银行卡");
Drawing drawing = new Drawing(account,50,"张三");
Drawing drawing1 = new Drawing(account,100,"李四");
drawing.start();
drawing1.start();
}
}

class Account{
int money; //余额
String name; //卡名

public Account(int money, String name) {
this.money = money;
this.name = name;
}
}

class Drawing extends Thread{
Account account; //账户
int drawingMoney; //取了多少钱
int nowMoney; //现在有多少钱

public Drawing(Account account,int drawingMoney,String name){
super(name);
this.account=account;
this.drawingMoney=drawingMoney;
}

@Override
public void run() {
//使用synchronized代码块
synchronized (account){
//判断有没有钱
if(account.money-drawingMoney<0){
System.out.println("余额不足!");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money=account.money-drawingMoney; //银行卡余额
nowMoney+=drawingMoney; //现在手里有的钱
System.out.println(this.getName()+"取了"+drawingMoney+"钱");
System.out.println(this.getName()+"现在手里有"+nowMoney+"钱");
System.out.println(account.name+"余额为"+account.money);
}
}

}

image-20210410105800229

使用同步代码块改造不安全的集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package demo6;
/**
* 不安全的集合
* 使用synchronized代码块改造
*/

import java.util.ArrayList;
import java.util.List;

public class Demo1 {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(3000);
System.out.println(list.size());
}
}

image-20210410113455838

JUC下的CopyOnWriteArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package demo6;
/**
* 测试JUC下的安全类型的集合
*/

import java.util.concurrent.CopyOnWriteArrayList;

public class Demo2 {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(3000);
System.out.println(list.size());
}
}

6.线程锁

死锁

  • 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有两个以上对象的锁时,就可能发生死锁问题

产生死锁的四个必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
  4. 循环等待条件,若干进程之间形成一种头尾相接的循环等待资源关系。

这是死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生

模拟死锁的发生

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
package demo6;

/**
* 模拟死锁的发生
*/
public class Demo3 {
public static void main(String[] args) {
Game game1 = new Game(0,"张三");
Game game2 = new Game(1,"李四");
game1.start();
game2.start();
}
}
class Play1{ //魂斗罗
}
class Play2{ //忍者神龟
}
class Game extends Thread{
int choose; //选择
String name; //玩游戏的人

public Game(int choose,String name){
this.name = name;
this.choose = choose;
}
//需要的资源只有一份,用static来保证唯一性
static Play1 play1 = new Play1();
static Play2 play2 = new Play2();

@Override
public void run() {
try {
playGame();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//编写一个玩游戏的方法,互相持有对方的游戏
public void playGame() throws InterruptedException {
if(choose==0) {
//获得魂斗罗的锁
synchronized (play1) {
System.out.println(this.name + "正在玩魂斗罗");
sleep(1000);
//一秒钟后想获得忍者神龟的锁
synchronized (play2){
System.out.println(this.name+"我还想玩忍者神龟");
}
}
}else{
//获得忍者神龟的锁
synchronized (play2) {
System.out.println(this.name + "正在玩忍者神龟");
sleep(1000);
//一秒钟后想获得魂斗罗的锁
synchronized (play1){
System.out.println(this.name+"我还想玩魂斗罗");
}
}
}
}
}

image-20210410155633704

由于两个线程都想持有对方的锁,都不肯释放自己的锁,所以导致两个线程僵持,就造成程序卡死。

解决办法:

不要让线程互相持有对方需要的锁,将对方需要的锁拿出代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void playGame() throws InterruptedException {
if(choose==0) {
//获得魂斗罗的锁
synchronized (play1) {
System.out.println(this.name + "正在玩魂斗罗");
sleep(1000);
//一秒钟后想获得忍者神龟的锁
}
synchronized (play2){
System.out.println(this.name+"我还想玩忍者神龟");
}
}else{
//获得忍者神龟的锁
synchronized (play2) {
System.out.println(this.name + "正在玩忍者神龟");
sleep(1000);
//一秒钟后想获得魂斗罗的锁
}
synchronized (play1){
System.out.println(this.name+"我还想玩魂斗罗");
}
}
}

image-20210410155539002

Lock(锁)

  • 从jdk5.0开始,Java提供了更强大的线程同步机制—通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。

不安全的买票

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
package demo6;

/**
* 不安全的买票
*/
public class Demo4 {
public static void main(String[] args) {
Tick tick = new Tick();
new Thread(tick,"张三").start();
new Thread(tick,"黄牛").start();
new Thread(tick,"小明").start();
}
}
class Tick implements Runnable{
int tickNum = 10;
@Override
public void run() {
while (true){
if(tickNum<0){
break;
}else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买到了第"+tickNum--+"张票");
}
}
}
}

image-20210410165731335

ps:我们可以看到票有重复的而且数据还乱了

使用lock加锁解决

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
package demo6;

import java.util.concurrent.locks.ReentrantLock;

/**
* 不安全的买票,使用Lock加锁实现线程安全
*/
public class Demo4 {
public static void main(String[] args) {
Tick tick = new Tick();
new Thread(tick,"张三").start();
new Thread(tick,"黄牛").start();
new Thread(tick,"小明").start();
}
}
class Tick implements Runnable{
int tickNum = 10;
//定义Lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
//加锁
lock.lock();
if(tickNum<=0){
break;
}else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买到了第"+tickNum--+"张票");
}
}finally {
//解锁
lock.unlock();
}
}
}
}

image-20210410170443448

ps:使用Lock加锁,大家就遵守规则排队买票啦

synchronized与Lock对比

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:
    • Lock>同步代码块(已经进入方法体,分配了相应资源)>同步方法(在方法体之外)

7.线程协作

线程通信

  • 应用场景:生产者和消费者问题

    • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
    • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
    • 如果仓库中放油产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止
  • Java中提供了几个方法解决线程之间的通信问题

    • wait() –表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁

    • wait(long timeout) –指定等待的毫秒数

    • notify() –唤醒一个处于等待状态的线程

    • notifyAll() –唤醒同一个对象上所调用wait()方法的线程,优先级别高的线程优先调度

    • 注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStateException

线程通信问题分析

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间互相依赖,互为条件

  • 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又要马上通知消费者消费

  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以共消费

  • 在生产者消费者问题中,仅有synchronized是不够的

    • synchronized可阻止并发更新同一个共享资源,实现了同步
    • synchronized不能用来实现不同线程之间的消息传递(通信)

解决方式1

并发协作模型“生产者/消费者模式”—>管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package demo6;

import com.sun.xml.internal.bind.v2.model.core.ID;

/**
* 生产者消费者模型 --> 利用缓冲区解决,管程法
*/
public class Demo5 {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
//生产者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container){
this.container=container;
}

@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
container.push(new Chicken(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产了"+i+"只鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container=container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
System.out.println("消费了"+container.pop().id+"只鸡");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//产品
class Chicken{
int id;

public Chicken(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer{
//定义容器大小
Chicken[] chickens = new Chicken[10];
//容器计数器
int count = 0;
//生产者放入产品
public synchronized void push(Chicken chicken) throws InterruptedException {
//如果容器满了就需要等待消费者消费
if(count==chickens.length){
//通知消费者消费,生产等待
this.wait();
}
//如果没有满,我们就需要丢入产品
chickens[count]=chicken;
count++;
//可以通知消费者消费了
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop() throws InterruptedException {
//判断是否能够消费
if(count==0){
//等待生产者生产,消费者等待
this.wait();
}
//如果可以消费
count--;
Chicken chicken = chickens[count];
//吃完了,通知生产者生产
this.notifyAll();
return chicken;
}

}

image-20210410214313938

ps:只有当生产者生产了鸡,消费者才能够消费,当生产者将容器生产满时,就停止生产并通知消费者消费,当消费者消费完时就又通知生产者生产。

解决方式2

并发协作模型“生产者/消费者模式”—>信号灯法

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package demo6;

/**
* 测试生产者消费者问题2:信号灯,标志位解决
*/
public class Demo6 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生产者-->演员
class Player extends Thread{
TV tv;
public Player(TV tv){
this.tv=tv;
}

@Override
public void run() {
for (int i = 0; i < 20; i++) {
if(i%2==0){
this.tv.play("快乐大本营播放中");
}else{
this.tv.play("抖音:记录美好生活");
}
}
}
}
//消费者-->观众
class Watcher extends Thread{
TV tv;
public Watcher(TV tv){
this.tv=tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品-->节目
class TV{
//演员表演,观众等待 T
//观众观看,演员等待 K
String voice; //表演的节目
boolean flag =true;

//表演
public synchronized void play(String voice){
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:"+voice);
//通知观众观看
this.notifyAll();
this.voice=voice;
this.flag = !this.flag;
}
//观看
public synchronized void watch(){
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了:"+voice);
//通知演员表演
this.notifyAll();
this.flag=!this.flag;
}

}

image-20210410214624999

ps:跟上面一样的

8.线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止

使用线程池

  • JDK5.0起提供了线程池相关的API:ExecutorService和Executors

  • ExecutorService:真正的线程池接口。常见的子类ThreadPoolExecutor

    • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
    • Futuresubmit(Callabletask):执行任务,有返回值,一般用来执行Callable
    • void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

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
package demo6;
/**
* 创建线程池
*/

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo7 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
service.submit(new MyThread());
service.submit(new MyThread());
service.submit(new MyThread());
service.submit(new MyThread());
}
}

class MyThread implements Runnable{

@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}

image-20210410221125405

  • Post title:多线程学习记录
  • Post author:周瑜
  • Create time:2021-04-12 14:13:55
  • Post link:https://xinblog.github.io/2021/04/12/多线程-md/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments