线程Thread

[toc]

Thread隶属于java.lang包下的类 ,java语言的JVM允许程序运行多个线程

Thread:每个线程通过特定Thread对象的run()方法来完成操作,经常把

程序,线程和进程

  • **程序(Program):**为了完成特定任务, 用某种语言编写的一组指令集合. 即一段静态代码,静态对象

  • **进程(process):**是指程序的一次执行过程, 或是正在运行的一个程序. 动态过程: 有它自身的产生, 存在和消亡的过程.

    如: 运行中的软件qq 浏览器等

    程序是静态的,运行的进程是动态的

  • **线程(Thread):**进程细化一些为线程,是一个程序内部的一条执行路径

    当热程序在同一时间可以有多个执行路径, 也就是支持多线程

基本概念

java中线程分为:守护线程(daemon)用户线程

虚拟机必须保证用户线程执行完毕,守护线程不需要等待: 如后台记录日志,内存监控,gc垃圾回收等

守护线程设置: thread.setDemon(true为守护线程)

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

线程的生命周期

在JDK中Thread.State枚举表示了线程的六种状态

getState():获取状态:hamburger:

  • 让我们看一下源码, 看一下线程的几个状态

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// java中hread的内部枚举
public enum State {
// 线程被声明并且创建后, 尚未启动的线程的线程状态。
NEW,
// 可运行的状态, 一般执行了star() 在虚拟机中执行等待分配资源 (如果执行了会执行run方法)
RUNNABLE,
// 阻塞了
BLOCKED,
// 等待
WAITING,
// 也是等待 但是有等待时间的等待
TIMED_WAITING,
//线程结束 终止
TERMINATED;
}

如图

线程生命周期

线程生命周期

线程的三种创建方式

Junit4单元测试线程异常: 具体原因不太了解但是和junit的实现有关(记到小本本–下次在了解)

一 . 继承Thread类

重写run()方法 ,并且用thread.start()启动线程

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadBuild {
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
threadTest.start();// 启动线程
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}

class ThreadTest extends Thread {
// 重写run方法
@Override
public void run() {
Thread.currentThread().setName("线程1");// 线程命名
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);// 获取当前线程名 + 变量
}
}
}

程序中创建了一个 线程1 线程 在主线程中启动 线程间是独立执行的 ,如图:

执行方式图解

执行方式图解

所以会看到并行交替执行的情况:

并没有顺序

并没有顺序

在Thread中有以下代码保证了Start()方法不能重复调用:

start的源码

start的源码

练习:

用多线程写图片复制/用线程实现3个售票窗口(要考虑产生资源冲突)(代码emmmm就不粘了)

二. 实现Runnable接口

实现Runnable接口并且重写run()方法

由于Thread类为一个代理类 代理了Runnable

java

1
2
3
4
5
6
7
8
// 实现接口的线程创建方式
ThreadTest threadTest = new ThreadTest();// Runnable接口实现类对象
// 将此对象作为形参放入 Thread的构造方法中 并创造对象
Thread thread1 = new Thread(threadTest);
Thread thread2 = new Thread(threadTest);
thread1.setName("1Thread");// 命名
thread1.start(); // 启动线程
thread2.start();

java

1
2
3
4
// lamdba表达式(1.8新特性)
new Thread(() -> {
System.out.println("run方法");
}).start();
  1. Thread 本身就实现了Runnable接口
  2. 实现方式优于继承方式
    1. 避免了java中单继承的局限性
    2. 在操作同一份资源中,更适合使用实现方式
      1. 一个对象的资源给多个线程使用

三. 实现Callable接口

重写的call()方法

需要执行服务和关闭服务(ExecutorService的对象 )
ExecutorService service = Executors.newFixedThreadPool(3);

提交执行 submit(线程对象)
Future f1 = service.submit(c);

获取返回值 记得结束服务
String s1 = (String) f1.get();service.shutdown();

Thread类常用方法

  1. start(): 启动线程并执行响应的run()方法

  2. run():子线程要执行的代码

  3. currentThread(): 静态的,调用当前的线程

  4. getName()/setName(): 获取和设置线程名字

  5. yield(): 调用此方法的线程放弃当前cpu的执行权,礼让别的线程(我让了但是,你们抢不抢得到那不归我管)

  6. join(): 在A线程中调用B.join()方法. (插入A线程中)

    表示: A线程停止,直到B线程执行完毕 A线程在执行后面的代码

    ==sleep()和yield()方法==

    • sleep不考虑优先级/yield同优先级

    • sleep 后是阻塞/yield是就绪状态

    • sleep有异常 /而且不会释放线程 – 都不会释放标志锁

    • sleep比yield有更好的移植性

      wait(): 当前线程等待执行wait的线程(要加同步锁 谁等就锁谁)

    • 和notify()/notifyAll() 一起用

    • 必须在synchronized中使用

    • wait会释放标志锁/sleep和yield不会

    • sleep是Thread的 / wait是Object的

    • sleep有异常需要捕获

  7. isAlive(): 判断当前线程是否存活

  8. sleep(long time): 当前线程睡眠time毫秒(1000ms = 1s)

    1. 不会释放线程
  9. 线程通信中wait()

  10. notify():释放优先级高的等待的线程

  11. notifyAll():释放所有等待的线程

  12. 设置线程的优先级

    getPriority(): 返回当前线程优先级

    setPriority(int newPriority): 改变当前线程优先级 : 正常为5,最大10,最小是1

线程安全

保证线程安全

  • 尽量避免共享资源的存取冲突, 如果必须有共享资源, 那就设计一个规则(锁)来保证, 同时间只有一个线程访问资源 而且一个客户的计算工作由一个线程解决

线程不安全的集合:

线程不安全 对应线程安全
1 ArrayList CopyOnWriterArrayLis/vector
2 HashMap HashTable
3 StringBuilder StringBuffer
  1. Servlet/Controller线程不安全的

刚才的窗口买票程序就会出现安全问题

原因:由于多个线程在操作共享的数据时,在其中一个线程未执行完毕时,另一个线程进入,导致共享数据出现问题

线程安全出现的原因.png

线程安全出现的原因.png

  • 如何解决线程安全的问题?

    想办法热那个一个线程操作数据完毕后,其他线程才能操作

  • java中实现线程安全,线程同步机制:

    • 一 同步代码块

      synchronized关键字 — 要有一个对象充当同步监视器(缺点:会影响效率)

      同步监视器:由一个类的对象充当.当一个线程获取监视器,就执行代码.(通一把锁的对象一定是同一个对象) —

      synchronized.png

      synchronized.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
实现代码:

```java
public void run() {
while (true) {
synchronized (this) { // 用本类的对象为同步监视器
if (ticket > 0) { // 如果有票 修改变量
try {
Thread.sleep(10); // 假设睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
}
}
}
```
  • 二 同步方法

    保证同一时间只有一个线程访问此方法

    用synchronized修饰方法: public synchronized void method()叫同步方法

    同步监视器对象默认为当前对象

  • 在单例模式中 – 锁的问题(双重锁定)

死锁

  • 概念: 不同的线程分别占用对方的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁
  • 解决办法:
    • 算法, 原则
    • 尽量减少同步资源的定义

LOCK锁

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

Lock与synchronized的对比

  • Lock是显式锁(需要手动开关) synchronized是隐式锁,自动释放
  • Lock只是锁代码块
  • 使用Lock, JVM调度线程花费时间少,性能好. 而且具有更好的扩展性(提供更多的子类)
  • 优先级:
    • Lock > 同步代码块>同步方法

java

1
2
3
4
5
6
7
8
9
10
11
private final ReentrantLock lock = new ReentrantLock();

public void show2() {
try { // 官方建议用Try,catch, finally
lock.lock();// 加锁
// 需要加锁的代码区
} finally {
lock.unlock();// 解锁
}

}

ThreadLocal

一种处理并发数据的方式 将数据 隔离 每个线程单独持有

  1. 用空间换时间
  2. 创建副本进行数据隔离
  • 要求是多个线程用一个对象,但是每个线程的对象是独立的

  • 每个线程独立的改变自己的变量副本,不去影响其他线程

  • Thread里面包含 ThreadLocal

    image-20200720211033368.png

    image-20200720211033368.png

    image-20200720211926178.png

    image-20200720211926178.png

  • 内部类ThreadLocalMap: k-v组成的entry[]数组

  • key是ThreadLocal的弱引用

    • val就是值

get: 获取

set: 存储

remove: 移除

initialValue :重写

  • 内存泄漏问题

    由于key的Thread是弱引用 所以会存在key是null 但是val是强引用 就会内存泄漏

    解决方法:是每一次get和set,remove时都会清楚为null的val 下一次垃圾回收就会清除了

    所以调用一下remove就行了

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   // 创建线程池
ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + "===" + DateUtilSafe.parsesss("2020-20-20 21:50:90"));
});
.................
service.shutdown();
// 工具类
public class DateUtilSafe {
// 写这个才安全
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-DD HH:mm:ss"));

public static Date parsesss(String dateStr) {
Date date = null;
try {
date = THREAD_LOCAL.get().parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}

线程通信

关键字:wait(), notify()和 notifyAll() – 线程调度的方法

  • wait(): 令当前线程挂起cpu/同步资源器, 使别的线程可以访问并修改公共资源,当前线程会在排队等待状态.
  • notify():唤醒正在排队等待同步资源的的优先级最高的线程 – 结束等待
  • notifyAll(): 唤醒正在排队等待同步资源的的所有线程 – 结束等待

注意: 这三个方法为java.lang.Object下的 而且只有在synchronized方法或代码块中才会使用.

写个生产者/消费者练习(不是OO的Pattern)

image.png

image.png

分析:

markdown

1
2
3
4
1. 是否考虑线程
2. 是否涉及共享数据
3. 如果有共享数据 考虑线程安全/同步的问题
4. 是否有线程通信

java

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
94
95
96
97
98
99
100
101
102
103
104
// 管城法
// 生产者做东西 消费者买 (通信:生产到20 停止生产 0开始生产停止购买)
public class P2CTest {
public static void main(String[] args) { // 主线程
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
Thread p1 = new Thread(producer);
Thread p2 = new Thread(producer);
Thread c1 = new Thread(consumer);
Thread c2 = new Thread(consumer);
Thread c3 = new Thread(consumer);
p1.setName("生产1");
p2.setName("生产2");
c1.setName("消费1");
c2.setName("消费2");
c3.setName("消费3");
p1.start();
p2.start();
c1.start();
c2.start();
c3.start();
}
}

class Clerk { // 店员 公共部分
int product;// 生产到20 停止生产 0开始生产停止购买
// 同步方法
public synchronized void addProduct(int num) {
if (product >= 20) { // 如果作满了 停止制作
System.out.println("暂停制作");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
product += num;// 添加制作数量
System.out.println(Thread.currentThread().getName() + "生产了: " + num + "====现在还有" + product);
if (product >= 1) { // 如果有存货就释放锁 让用户购买
notifyAll();
}
}

}

public synchronized void buyProduct(int num) {
if (product <= 0) {
System.out.println("暂停购买");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
product -= num;
System.out.println(Thread.currentThread().getName() + "购买了了: " + num + "====现在还有" + product);
if (product < 20) { // 如果数量不足释放生产者来制作
notifyAll();
}
}

}
}

class Producer implements Runnable {// 生产者
Clerk clerk;

public Producer(Clerk clerk) {
this.clerk = clerk;
}

@Override
public void run() {
while (true) {
clerk.addProduct(1);
}
}
}

class Consumer implements Runnable { // 消费者
Clerk clerk;

public Consumer(Clerk clerk) {
this.clerk = clerk;
}

@Override
public void run() {
while (true) {
clerk.buyProduct(1);
}
}
}

出现的问题

在等待中推荐使用wait 如果使用 if 将会有肯能出现虚假唤醒问题:

  • 线程可以唤醒但是不会被通知, 中断或者超时,即所谓的虚假唤醒. 发生概率虽然还很小, 但是程序必须通过测试应该使线程被唤醒的条件来防范, 并且如果条件不满足则继续等待.

  • 等待应该总是出现在循环中

  • 原因:

    本质在于 if 和while 的区别 : if中挂起了 如果被唤醒会继续执行但是这个唤醒有可能不正确

    1. 两个消费者线程发现剩余0 于是唤醒生产者 自己挂起;(if判断)
    2. 生产者生产一个 唤醒所有消费者 两个消费者 不管谁先获得锁 都是继续if 执行
    3. 但是 其中一个消费过后 下一个应该挂起但是没有了判断 所以被虚假唤醒了
  • 解决 把等待放到while中 让他循环判断

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
if (product >= 20) {
wait();
}
*/
修改为:
synchronized(obj){
while(product >= 20){
obj.wait();
}
// ---------------
if (product >= 1) {
notifyAll();
}
}

线程池

  • 将线程放入池子,方便使用和管理,也避免了销毁造成的浪费
  • 3.Java线程池(工具)
  • 线程池有很多种,但是核心线程池ThreadPoolExecutor。
  • ExecutorService:线程池接口
  • Executors(Runable 接口):工具类,线程池工厂

java

1
2
3
4
5
6
7
8
9
10
//线程池:内含了一些线程数量,以及拒绝策略,阻塞队列...内容。

public ThreadPoolExecutor(int corePoolSize, //核心线程数,工作的
int maximumPoolSize, //最大线程数
long keepAliveTime, //最大活跃时间,如果超过最大活跃时间,不工作的线程被回收(优先回收非核心线程)
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂,创建线程的方式
RejectedExecutionHandler handler){ //拒绝策略
}
  1. 假设线程池中有20个线程,10个核心线程数,当有10个任务来临,优先使用核心线程来工作,如果又来1个任务:

    1. 第一,创建一个非核心线程工作
    2. 第二,进入阻塞队列
    3. 阻塞队列共有4种:S>L>A:
      1. SynchronousQueue:同步队列,如果有任务进入队列,直接创建一个线程工作。
      2. LinkedBlockingQueue:链表阻塞队列,如果有任务进入队列,直接排队,按照先进先出的规则进行执行,先排队,先执行。
      3. ArrayBlockingQueue:数组阻塞队列,如果有任务进入队列,直接排队,按照先进先出的规则进行执行,先排队,先执行。
      4. DelayQueue:延迟队列,如果有任务进入队列,先排队,但是不立即执行,而是等到延迟时间到了,再执行。
  2. 拒绝策略也有4种:当有任务来临时,如果超过了线程池的规定线程数,可以选择任务执行的策略。

    1. 直接丢弃(DiscardPolicy)
    2. 队列中最老的任务(DiscardOldestPolicy)
    3. 抛出异常(AbortPolicy),默认策略
    4. 将任务分配调用线程来执行(CallerRunsPolicy)

    java

    1
    public class ThreadPoolDemo {
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
//定义核心线程数
private static final int MAX_CORE = 4;
//定义最大线程数
private static final int MAX_THREAD_NUM = 10;
//定义活跃时间,单位毫秒
private static final long MAX_ACTIVE_TIME = 3000;
//定义阻塞队列
private static LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();

//定义一个线程执行器
private static ThreadPoolExecutor threadPoolExecutor;

//静态代码块
static {
//实例化一个线程池执行器
ThreadPoolExecutor executor = new ThreadPoolExecutor(MAX_CORE,
MAX_THREAD_NUM,
MAX_ACTIVE_TIME,
TimeUnit.MILLISECONDS,
workQueue
);
//允许核心线程超时,核心线程也在销毁之内
executor.allowCoreThreadTimeOut(true);
//将创建出来的线程池执行器赋值给成员变量
threadPoolExecutor = executor;
}


public static void main(String[] args) {
/*threadPoolExecutor = new ThreadPoolExecutor(MAX_CORE,
MAX_THREAD_NUM,
MAX_ACTIVE_TIME,
TimeUnit.MILLISECONDS,
workQueue);*/
for(int i = 1;i <= 20;i++){
Runnable r = new Runnable(){
public void run(){
try{
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
for(int i = 1;i <= 10;i++){
System.out.println("i = " + i);
}
}
};
threadPoolExecutor.execute(r);
}
}

}

1

java

1
2
3
4
5
ExecutorService service = Executors.newFixedThreadPool(3);// 数量
service.execute(new SJSThread);
service.execute(new SJSThread);
service.execute(new SJSThread);
service.shutdown();//结束线程连接池

同步/异步

  • 异步: (非阻塞)
    1. 当程序正在执行一个较长时间的程序, 不等待他的返回,就是异步
    2. 异步效率高.
  • 同步:(阻塞)
    1. 当程序中存在竞争资源, 或者正在读取的数据可能被修改 就使用同步存取
    2. 如果有公共数据就要用同步方法
    3. 数据库的排它锁