这个只是简单的学了一下怎么用,几个小demo,没有深入的讲。
1. 线程简介
线程,进程,多线程
- 并发:同时发生,在一个时间段内执行,不一定是同一时间点
- 并行:同时执行,在一个时间点上有多个线程执行
关于run()和start()
- 一个进程可以有多个线程
三个概念,程序、进程、线程:
- 程序:静态代码
- 进程:程序的一次执行过程
- 线程:一个进程可以包含多个线程, 线程是CPU调度和执行的单位。
很多多线程是模拟出来的,真正的多线程是指有多个CPU。模拟的线程只是一个CPU切换的很快产生了多线程的错觉。其实就是并发和并行的区别。
main()
函数就是主线程;- 默认的话有主线程、GC线程等;
- 线程因为调度,会代来开销
- 资源抢夺问题,需要并发控制
2. 线程实现(重点)
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
三种实现方式
java
public class TestThread1 extends Thread {
PS: 线程开启不一定立即执行,由CPU调度执行
java
public class TestThread2 extends Thread { private String url; private String name; public TestThread2(String url, String name) { this.name = name; this.url = url; } public static void main(String[] args) { TestThread2 testThread1 = new TestThread2("https://commons.apache.org/proper/commons-io/images/commons-logo.png", "1.png"); TestThread2 testThread2 = new TestThread2("https://commons.apache.org/proper/commons-io/images/io-logo-white.png", "2.png"); TestThread2 testThread3 = new TestThread2("http://www.apache.org/events/current-event-125x125.png", "3.png"); testThread1.start(); testThread2.start(); testThread3.start(); }
需要Apache的一个包 commons-io
:
下载后Add to Library。
java
public class TestThread3 implements Runnable { public static void main(String[] args) {
- 继承 Thread 类:
- 启动线程:子类对象.start()
- 不推荐使用:避免单继承局限性
- 实现 Runnable 接口:
- 启动线程:new Thread(对象) +
thread.start()
- 推荐使用:没有单继承局限性,方便同一个对象被多个线程使用
Runnable可以被重复使用
Thread:基于继承;Runnable:基于组合;创建Thread比一个Runnable成本要昂贵一点。/
卖票问题
Thread.currentThread.getName()
:得到当前正在执行的线程方法Thread.sleep(200)
:此线程暂停200ms
java
public class TestThread4 implements Runnable { private int ticketNums = 10; public static void main(String[] args) { TestThread4 t = new TestThread4(); new Thread(t, "小明").start(); new Thread(t, "Bob").start(); new Thread(t, "牛牛").start(); }
这里还没讲怎么解决,看之后的。
龟兔赛跑问题
java
public class Race implements Runnable{ private String winner;
Callable其实相当于一个增强的Runnable,带有返回结果。
java
public class TestCallable implements Callable<Boolean> {
private String url;
private String name;
public TestCallable(String url, String name) {
this.name = name;
this.url = url;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable tc1 = new TestCallable("https://commons.apache.org/proper/commons-io/images/commons-logo.png", "1.png");
TestCallable tc2 = new TestCallable("https://commons.apache.org/proper/commons-io/images/io-logo-white.png", "2.png");
TestCallable tc3 = new TestCallable("http://www.apache.org/events/current-event-125x125.png", "3.png");
继承Callable的时候会有个泛型,指定返回值的类型。
然后实现的是 call()
方法,带有返回值。
Callable步骤
多线程的 Thread 和 Runnable 就是静态代理模式。
在代理模式(Proxy Pattern)中, 一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。
主要解决:在直接访问对象时带来的问题
何时使用:想在访问一个类时做一些控制。
如何解决:增加中间层。
关键代码:实现与被代理类组合。
这里举个例子:
image-20210623152348219
java
3. 线程状态
线程的五大状态
Java中的线程状态变化
- 不推荐使用JDK提供的stop()、destroy()等方法。【已废弃】
- 推荐线程自己停止下来
- 建议使用一个标志位进行终止变量当flag=false,则终止线程运行。
标志位的方式:
java
sleep()
sleep(1000)
,休眠1000毫秒,也就是1s- 会抛出
InterruptedException
- 调用 sleep,线程进入阻塞状态;sleep 时间到达后,进入就绪状态
- sleep 不会释放对象的锁,wait 会释放对象锁
java
public class TestSleep{ // 模拟倒计时 public void timeCount(int second) throws InterruptedException { while (second > 0){ System.out.println(second--); Thread.sleep(1000); } } // 打印系统当前时间 public void timePrinter() throws InterruptedException { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); while (true){ System.out.println(dateFormat.format(new Date(System.currentTimeMillis()))); Thread.sleep(1000); } } public static void main(String[] args) throws InterruptedException { TestSleep testSleep = new TestSleep(); testSleep.timeCount(10); testSleep.timePrinter(); }}
- 礼让线程,让当前正在执行的线程暂停,但不阻塞将线程
- 从运行状态转为就绪状态
- 让cpu重新调度,礼让不一定成功!看CPU心情
java
public class TestYield { public static void main(String[] args) { Runnable runnable = ()->{ System.out.println(Thread.currentThread().getName() + " 线程开始执行"); Thread.yield(); System.out.println(Thread.currentThread().getName() + " 线程停止"); }; new Thread(runnable,"线程A").start(); new Thread(runnable,"线程B").start(); }}
PS:我礼让就没成功过。。电脑的原因吗
join()
合并线程,待此线程执行完之后,再执行其他线程,其他线程阻塞- 可以想象成插队
java
public class TestJoin implements Runnable { @Override public void run() { for (int i = 0; i < 5000; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Join线程-------------" + i); } } public static void main(String[] args) throws InterruptedException { TestJoin testJoin = new TestJoin(); Thread thread = new Thread(testJoin); thread.start(); for (int i = 0; i < 5000; i++) { // 当主线程 200 的时候,让thread插队 if(i == 1000){ thread.join(); } System.out.println("Main线程 + " + i); } }}
JDK1.8文档:
public static enum Thread.State extends Enum
- NEW 尚未启动的线程处于此状态。
- RUNNABLE 在Java虚拟机中 执行的线程处于此状态。
- BLOCKED 被 阻塞等待监视器锁定的线程处于此状态。
- WAITING 正在 等待另一个线程执行特定动作的线程处于此状态。
- TIMED_WAITING 正在 等待另一个线程执行动作达到指定等待时间的线程处于此状态。
- TERMINATED 已退出的线程处于此状态。 一个线程可以在给定时间点处于一个状态。 这些状态是不反映任何操作系统线程状态的虚拟机状态。
thread1.getState()
:得到 thread1 的状态,上面的那几个值
java
public class TestState { 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("start之前: " + state); thread.start(); state = thread.getState(); System.out.println("start之后:" + state); while (state != Thread.State.TERMINATED){ // 这里一定要更新 state = thread.getState(); System.out.println(state); Thread.yield(); Thread.sleep(1000); } state = thread.getState(); System.out.println("线程死了!--" + state); // 线程是一次性用品,死亡后不能再次启动 thread.start(); }}
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
- 线程的优先级用数字表示,范围从1~10.
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5;
- 使用以下方式改变或获取优先级
getPriority()
:获取优先级- *
setPriority(int xxx)
: 设置优先级
线程执不执行还是得看CPU,优先级高的不一定先执行。但是优先级高的权重大,更可能先执行。
- 性能倒置:优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度
java
public class TestPriority { public static void main(String[] args) { System.out.println(Thread.currentThread().getName() + " --> " + Thread.currentThread().getPriority() ); MyPriority myPriority = new MyPriority(); Thread t1 = new Thread(myPriority,"t1"); Thread t2 = new Thread(myPriority,"t2"); Thread t3 = new Thread(myPriority,"t3"); Thread t4 = new Thread(myPriority,"t4"); Thread t5 = new Thread(myPriority,"t5"); t1.setPriority(1); t2.setPriority(2); t3.setPriority(3); t4.setPriority(4); t5.setPriority(Thread.MAX_PRIORITY); t4.start(); t2.start(); t1.start(); t3.start(); t5.start(); }}class MyPriority implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + " --> " + Thread.currentThread().getPriority() ); }}
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕:如
main
线程 - 虚拟机不用等待守护线程执行完毕:如后台记录操作日志、监控内存、垃圾回收GC线程等..
thread1.setDaemon(true)
:将线程 thread1 设为守护线程
java
public class TestDaemon { public static void main(String[] args) { Bless bless = new Bless(); NormalT normalT = new NormalT(); Thread thread = new Thread(bless); // 默认false表示为正常线程,true表示设为守护线程 thread.setDaemon(true); Thread thread1 = new Thread(normalT); thread1.start(); thread.start(); // 可以看到,主线程和Normal线程结束之后,程序结束。守护线程死循环也终止了 }}class Bless implements Runnable{ @Override public void run() { while (true){ System.out.println("God Bless You"); } }}class NormalT implements Runnable{ @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("此线程存活"); } System.out.println("此线程死亡"); }}
4. 线程同步(重点)
并发:同一个对象被多个线程 同时操作
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候我们就需要线程同步。
线程同步其实就是一种 等待机制,多个需要同时访问此对象的线程进入 这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
- 队列 + 锁,解决线程同步的安全性。
synchronized,排他锁独占资源。
- 使用锁可能会引起问题:
- 一个线程持有锁会导致其他所有需要此锁的 线程挂起;
- 在多线程竞争下,加锁﹐释放锁会导致比较多的上下文切换和调度延时,引起 性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致 优先级倒置﹐引起性能问题.
产生死锁的四个必要条件:1.互斥条件:一个资源每次只能被一个进程使用。2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生
写个死锁案例:
java
public class DeadLock { public static void main(String[] args) { Integer i1 = 10; Integer i2 = 20; Thread thread = new Thread(new MyThread(i1, i2, 0), "线程1"); Thread thread2 = new Thread(new MyThread(i1, i2, 1), "线程2"); thread.start(); thread2.start(); }}class MyThread implements Runnable { private Integer i1; private Integer i2; private Integer choice; public MyThread(Integer i1, Integer i2, Integer choice) { this.i1 = i1; this.i2 = i2; this.choice = choice; } @Override public void run() { // 用一个choice,分别先拿不同资源 if (choice == 0) { // 先拿1,再拿2 synchronized (i1) { System.out.println(Thread.currentThread().getName() + "得到了i1: " + i1); // 将两个这个拿到外层,可以解决死锁 synchronized (i2) { System.out.println(Thread.currentThread().getName() + "得到了i2: " + i2); } } } else { // 先拿2,再拿1 synchronized (i2) { System.out.println(Thread.currentThread().getName() + "得到了i2: " + i2); synchronized (i1) { System.out.println(Thread.currentThread().getName() + "得到了i1: " + i1); } } } }}
将最后一段的代码改为:
java
@Overridepublic void run() { // 用一个choice,分别先拿不同资源 if (choice == 0) { // 先拿1,再拿2 synchronized (i1) { System.out.println(Thread.currentThread().getName() + "得到了i1: " + i1); } synchronized (i2) { System.out.println(Thread.currentThread().getName() + "得到了i2: " + i2); } } else { // 先拿2,再拿1 synchronized (i2) { System.out.println(Thread.currentThread().getName() + "得到了i2: " + i2); } synchronized (i1) { System.out.println(Thread.currentThread().getName() + "得到了i1: " + i1); } }}
这样就能避免死锁了。为啥呢?我自己理解的,是锁升级到重量级锁了,阻塞了其中的一个线程。
JUC,就是 import java.util.concurrent
类
java
public class TestLock { public static void main(String[] args) { TestLock2 testLock2 = new TestLock2(); new Thread(testLock2,"小明").start(); new Thread(testLock2,"小二").start(); new Thread(testLock2,"小王").start(); }}class TestLock2 implements Runnable{ private final ReentrantLock reentrantLock = new ReentrantLock(); int tirckNums = 10; @Override public void run() { while (true){ // 加锁 reentrantLock.lock(); try { if (tirckNums > 0){ try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+tirckNums--); }else { break; } }finally { // 解锁 reentrantLock.unlock(); } } }}
手动锁,自己调用 lock
和 unlock
,锁代码块。
- Lock是 显式锁(手动开启和关闭锁,别忘记关闭锁) ;synchronized是 隐式锁,出了作用域自动释放
- Lock只有 代码块锁,synchronized有 代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程, 性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
5. 线程通信问题
可以观看:
生产者消费者问题
问题分析
Java方法
java
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
Producer producer = new Producer(synContainer);
Consumer consumer =new Consumer(synContainer);
producer.start();
consumer.start();
}
}
/**
* 生产者
**/
class Producer extends Thread {
SynContainer synContainer;
public Producer(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
// 生产
for (int i = 0; i < 100; i++) {
synContainer.push(new Production(i));
System.out.println("生产者生产了--> "+i);
}
}
}
/**
* 消费者
**/
class Consumer extends Thread {
SynContainer synContainer;
public Consumer(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
int id = synContainer.pop().id;
System.out.println("消费者消费了--> " + id);
}
}
}
/**
* 产品
**/
class Production {
// 产品编号
int id;
public Production(int id) {
this.id = id;
}
}
/**
* 缓冲区
**/
class SynContainer {
// 缓冲区容器,总容量
Production[] productions = new Production[10];
// 当前存在的容量
int count = 0;
//生产者放入产品
public synchronized void push(Production production) {
// 缓冲区已满
if (count == productions.length) {
// 缓冲区已满,生产者停止生产;
// 通知消费者,进行消费
try{
this.wait();
}catch (Exception e){
e.printStackTrace();
}
}
// 放入
productions[count] = production;
count++;
// 生产出来了,通知消费者,可以立即消费
this.notifyAll();
}
// 消费者取走产品
public synchronized Production pop() {
if (count == 0){
// 通知生产者生产
try{
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
// 取出
count--;
Production production = productions[count];
// 取出之后,缓冲区有空格了
this.notifyAll();
return production;
}
}
Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而 管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是 管程利用OOP的封装特性解决了信号量在工程实践上的复杂性问题,因此java采用管理机制。
所谓 管程,指的是管理共享变量以及对其操作过程,让它们支持并发访问。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。
有一点需要再次提醒,对于 MESA 管程来说,有一个编程范式,就是==需要在一个 while 循环里面调用 wait()==。 这个是 MESA 管程特有的。
锁和 信号量(Semaphore)是实现多线程同步的两种常用的手段。
信号量需要初始化一个许可值,许可值可以大于0,也可以小于0,也可以等于0.
- 如果大于0,表示,还有许可证可以发放,线程不会被阻塞;
- 如果小于或者等于0,表示,没有许可证可以发放了,线程被阻塞住了。
它有两个常用的操作:
acquire()
申请许可证,如果有,就可以获得,如果没有就等待了。相当于减法。release()
归还许可证,保证循环使用。相当于加法。
看一个例子,就会明白了,还是实现上次的那个生产者和消费者的例子。
java
package com.songx64.baselearn.threadlearn.kuangThread.gaoji;
import java.util.LinkedList;
import java.util.concurrent.Semaphore;
/**
* Created on 2021/6/26,上午 11:03
* 信号量机制 Semaphore
*
* @author SongX64
*/
public class TestPC2 {
public static void main(String[] args) {
SynContainer2 synContainer2 = new SynContainer2();
Thread p1 = new Producer2(synContainer2);
Thread c1 = new Consumer2(synContainer2);
Thread c2 = new Consumer2(synContainer2);
p1.start();
c1.start();
c2.start();
}
}
/**
* 产品
**/
class Production2 {
public int id;
public Production2(int id) {
this.id = id;
}
}
/**
* 生产者
**/
class Producer2 extends Thread {
SynContainer2 synContainer2;
public Producer2(SynContainer2 synContainer2) {
this.synContainer2 = synContainer2;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
synContainer2.push(new Production2(i));
System.out.println("生产了-->" + i);
}
}
}
class Consumer2 extends Thread {
SynContainer2 synContainer2;
public Consumer2(SynContainer2 synContainer2) {
this.synContainer2 = synContainer2;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
int id = synContainer2.pop().id;
System.out.println(Thread.currentThread().getName()+"消费了 }
}
}
/**
* 缓冲区
**/
class SynContainer2 {
// 容器
LinkedListproduction2s = new LinkedList<>();
// 互斥信号量,保证安全性
Semaphore mutex = new Semaphore(1);
// 为满信号量,初始是最大容量
Semaphore isFull = new Semaphore(10);
// 为空信号量
Semaphore isEmpty = new Semaphore(0);
// 生产者生产,为满阻塞
public void push(Production2 production2) {
try {
//大于0,就放行
//acquire,就是减操作,如果 //release,就是加操作,如果 >0,就不会被阻塞
isFull.acquire();
// 添加操作是互斥的,因为占用了缓冲区
mutex.acquire();
int i = isFull.availablePermits();
production2s.add(production2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放互斥锁
mutex.release();
// 生产完成后,让空的增加,可以进行消费
isEmpty.release();
}
}
// 消费者消费,为空阻塞
public Production2 pop() {
Production2 temp = null;
// 为空减少一个信号量,如果是空的0再减就阻塞了;
try {
isEmpty.acquire();
mutex.acquire();
temp = production2s.removeLast();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mutex.release();
isFull.release();
}
return temp;
}
}
写的有点乱。。。但是知道Semaphore的用法就行了
6.高级主题
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对 性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理(….)
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
Java线程池类
简单使用:
java
public class TestPool { public static void main(String[] args) { // 1.1 手动创建线程池(建议),指定参数 // 参数:核心线程数,最大线程数,核心线程外的工作线程存活时间,时间单位,阻塞队列 ThreadPoolExecutor executorService = new ThreadPoolExecutor( 10, 20, 100, TimeUnit.SECONDS, new LinkedBlockingDeque<>()); // 1.2 自动创建线程池(不建议),10个核心线程/最大线程 //ExecutorService executorService = Executors.newFixedThreadPool(10); executorService.execute(new MyThread()); executorService.execute(new MyThread()); executorService.execute(new MyThread()); executorService.execute(new MyThread()); }}class MyThread implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); }}
Original: https://www.cnblogs.com/wscsdn/p/15930994.html
Author: 扬帆起航$
Title: 多线程学习
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/580553/
转载文章受原作者版权保护。转载请注明原作者出处!