Java学习-08 多线程学习
1、线程和进程
1.1、基本概念:
我们在使用电脑的时候,往往会打开很多应用程序,比如你一边听着歌一边打着游戏,同时可能还开着浏览器,那么每个运行中的程序就是一个进程。当一个程序运行时,比如你在使用音乐软件听歌的时候,可能一边下载这喜欢的歌曲,一边听着歌,一边还在看评论,那这就是“同时”在进行的一些操作,它们就对应一个个线程,因此一个应用程序(进程)内部可能包含了多个线程,每个线程都是一个顺序执行流。
比如win10系统打开任务管理器:
多个进程:
打开性能可以看到我现在有223个进程,但是有2483个线程,所以每个进程都有至少一个或多个线程。
当一个应用程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位。
线程(Thread)也被称作轻量级进程(LigheweightProcess),线程是进程的执行单元。当进程被初始化后,主线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个主线程,但我们也可以在该进程内创建多个线程,用来操作不同的事情,每条线程也是互相独立的。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不再拥有系统资源,它与父进程的其它线程共享该进程所拥有的全部资源。
简而言之:一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少包含一个线程。
1.2、多线程
多线程的优势:进程间不能共享内存,但线程之间共享内存非常容易。当我们使用浏览器,音乐播放器,打游戏等等的时候,都需要在同一时间进行各种不同的操作,实现这些操作的便是一个个线程,它让我们对电脑,对手机的使用更加便捷,所以多线程的应用是十分广泛的。
那么多线程在计算机里是怎么调度的?是真的同时在执行多个不同的线程吗?
线程调度
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心(我们通常所说的八核CPU就是有八个核心,可以同时执行八个线程)而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快(这个可以想象一下马戏团中的抛球,同时抛好多个球,但是因为速度很快,所以在我们看来就是一个球),看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
1.3、同步和异步
同步的概:同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。同字从字面上容易理解为一起动作其实不是,“同”字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A,A再继续操作。
异步就是和同步相对的了,不用等待谁返回结果,直接就可以运行。
同步:排队执行 , 效率低但是安全.
异步:同时执行 , 效率高但是数据不安全.
1.4、并发和并行
并发:指两个或多个事件在同一个时间段内发生。我们的多线程基本都是并发的,是CPU在调度。
并行:指两个或多个事件在同一时刻发生(同时发生)。
2、实现多线程:Thread、Runable、Callable
2.1、Thread实现
通过继承Thread类,需要重写Thread的Run方法。
直接上代码:
/**
* @Author: deemoHui
* @Description: 继承Thread实现多线程
* @Date Created in 2020-08-09 22:05
* @Modified By:
*/
public class MyThreadDemo extends Thread {
//run方法就是线程要执行的任务方法
@Override
public void run() {
//这里的代码就是一条新的执行路径
//这个执行路径是触发方式,不是调用run方法,而是通过thread对象的start方法来启动任务
for (int i = 0; i < 10; i++) {
System.out.println("唱歌---lalalalala" + i);
}
}
}
测试一下效果(这里的结果具有随机性,原因是这个代码简单,CPU可能就直接执行完事一个再执行另一个):
/**
* @Author: deemoHui
* @Description: 测试多线程
* @Date Created in 2020-08-09 22:09
* @Modified By:
*/
public class Multithreading {
public static void main(String[] args) {
MyThreadDemo t = new MyThreadDemo();
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("跳舞---左扭右扭" + i);
}
}
}
/*
唱歌---lalalalala0
跳舞---左扭右扭0
唱歌---lalalalala1
跳舞---左扭右扭1
唱歌---lalalalala2
跳舞---左扭右扭2
唱歌---lalalalala3
跳舞---左扭右扭3
唱歌---lalalalala4
跳舞---左扭右扭4
唱歌---lalalalala5
跳舞---左扭右扭5
唱歌---lalalalala6
跳舞---左扭右扭6
唱歌---lalalalala7
跳舞---左扭右扭7
唱歌---lalalalala8
跳舞---左扭右扭8
跳舞---左扭右扭9
唱歌---lalalalala9
*/
2.2、Runable实现
实现Runnable接口,重写run方法。 然后可以创建类的实例,之后创建Thread时作为参数传递,然后启动。
案例代码:
/**
* @Author: deemoHui
* @Description:
* @Date Created in 2020-08-09 22:30
* @Modified By:
*/
public class MyRunnableDemo implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("唱战歌第" + (i + 1) + "句");
}
}
}
/**
* @Author: deemoHui
* @Description: 测试多线程
* @Date Created in 2020-08-09 22:09
* @Modified By:
*/
public class Multithreading {
public static void main(String[] args) {
// 创建一个任务对象
MyRunnableDemo r = new MyRunnableDemo();
// 创建一个线程并给他一个任务
Thread t = new Thread(r);
// 这里开启子线程
t.start();
// 这里是主线程
for (int i = 0; i < 10; i++) {
System.out.println((i + 1) + "杀!");
}
}
}
/*
唱战歌第1句
1杀!
唱战歌第2句
2杀!
唱战歌第3句
唱战歌第4句
唱战歌第5句
唱战歌第6句
唱战歌第7句
唱战歌第8句
唱战歌第9句
唱战歌第10句
3杀!
4杀!
5杀!
6杀!
7杀!
8杀!
9杀!
10杀!
*/
2.3、Thread和Runnable的区别
实现Runnable与继承Thread相比有如下优势:
1、通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况
2、可以避免单继承所带来的局限性
3、任务与线程是分离的,提高了程序的健壮性
4、后面的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程
3、Thread类
3.1、构造器
Thread() | 分配新的 Thread 对象。 |
---|---|
Thread(Runnable target) | 分配新的 Thread 对象。 |
Thread(Runnable target, String name) | 分配新的 Thread 对象。 |
Thread(String name) | 分配新的 Thread 对象。 |
可以看到,创建Thread的时候可以传递一个name的属性,就是设置这个Thread的名称,不设置的话就是0,1,2这样
3.2、字段
变量和类型 | 字段 | 描述 |
---|---|---|
static int | MAX_PRIORITY | 线程可以拥有的最大优先级。 |
static int | MIN_PRIORITY | 线程可以拥有的最低优先级。 |
static int | NORM_PRIORITY | 分配给线程的默认优先级。 |
3.2、主要方法
返回值类型 方法 描述
- static Thread currentThread() 返回对当前正在执行的线程对象的引用。
- String getName() 返回此线程的名称。
- int getPriority() 返回此线程的优先级。
- Thread.State getState() 返回此线程的状态。
- ThreadGroup getThreadGroup() 返回此线程所属的线程组。
- void interrupt() 中断此线程。
- void setDaemon(boolean on) 将此线程标记为 daemon线程或用户线程。
- void setName(String name) 将此线程的名称更改为等于参数 name 。
- void setPriority(int newPriority) 更改此线程的优先级。
- static void sleep(long millis) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。
- static void sleep(long millis, int nanos) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。
- void start() 此线程开始执行; Java虚拟机调用此线程的run方法。
- String toString() 返回此线程的字符串表示形式,包括线程的名称,优先级和线程组。
3.3、方法测试
3.3.1、线程名称获取和设置:
主要是使用currentThread()、getName()、setName,以及带name的构造方法
/**
* @Author: deemoHui
* @Description: 线程名称测试
* @Date Created in 2020-08-10 10:17
* @Modified By:
*/
public class ThreadNameDemo {
public static void main(String[] args) {
// 获取线程名称
System.out.println("线程名称是:" + Thread.currentThread().getName());
// 使用继承类,并设置名称
Thread t1 = new Thread(new MyThread2(), "singing");
t1.start();
// 使用接口方法,并设置名称
Thread t2 = new Thread(new MyRunnable2(), "dancing");
t2.start();
// 先创建,再设置名称
Thread t3 = new Thread(new MyRunnable3());
t3.setName("使用Set设置名称");
t3.start();
// 不设置名称
new Thread(new MyRunnable3()).start();
}
}
class MyThread2 extends Thread{
@Override
public void run() {
System.out.println("线程名称是:" + Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println("唱歌---lalalalala" + i);
}
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
System.out.println("线程名称是:" + Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println("跳舞---波次八次" + i);
}
}
}
class MyRunnable3 implements Runnable{
@Override
public void run() {
System.out.println("线程名称是:" + Thread.currentThread().getName());
}
}
/*
线程名称是:main
线程名称是:singing
线程名称是:dancing
线程名称是:使用Set设置名称
线程名称是:Thread-2
唱歌---lalalalala0
唱歌---lalalalala1
唱歌---lalalalala2
唱歌---lalalalala3
唱歌---lalalalala4
跳舞---波次八次0
跳舞---波次八次1
跳舞---波次八次2
跳舞---波次八次3
跳舞---波次八次4
*/
3.3.2、线程休眠
主要是使用sleep()方法
/**
* @Author: deemoHui
* @Description: 线程休眠测试
* @Date Created in 2020-08-10 10:48
* @Modified By:
*/
public class ThreadSleepDemo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
System.out.println("现在是" + i);
// 传进去的是ms 1s = 1000 ms
Thread.sleep(1000);
}
}
}
/* 运行的时候可以明显的感觉到时间上的差异
现在是0
现在是1
现在是2
现在是3
现在是4
*/
需要注意的是,凡是会耗时的操作都会导致线程的阻塞,比如之前我们接收用户输入的时候,不按Enter键,就一直在等待用户输入,这个时候其实就是主线程的阻塞。
3.3.3、线程中断
原本有一个stop方法可以直接杀死线程,但是这个方法不安全,所以已经过时了,不要用,它会引起资源的不释放。
那么我们如何中断线程呢?首先使用interrupt() 打上中断标记,那么它会引起一个InterruptedException异常,我们捕获这个异常,并对资源进行释放(子线程交代后事),之后在方法内使用return。
演示代码:
/**
* @Author: deemoHui
* @Description: 线程中断
* @Date Created in 2020-08-10 11:04
* @Modified By:
*/
public class MyInterruptDemo {
public static void main(String[] args) {
// 一个线程是一个独立的执行路径,它是否结束应该由其自身决定
Thread t1 = new Thread(new MyRunnable4());
t1.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//给线程t1添加中断标记
t1.interrupt();
}
static class MyRunnable4 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// System.out.println("发现了中断标记,但是不死亡,向天再借500年");
System.out.println("发现了中断标记,线程交代后事后死亡");
// 不return就不死,但是最好在此之前交代完后事
return;
}
}
}
}
}
/*不使用return杀死线程,注释掉return和发现了中断标记,线程交代后事后死亡
main:0
Thread-0:0
main:1
Thread-0:1
main:2
Thread-0:2
main:3
Thread-0:3
main:4
Thread-0:4
发现了中断标记,但是不死亡,向天再借500年
Thread-0:5
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
*/
/* 使用return杀死线程,注释掉向天再借500年
Thread-0:0
main:0
Thread-0:1
main:1
Thread-0:2
main:2
main:3
Thread-0:3
Thread-0:4
main:4
Thread-0:5
发现了中断标记,线程交代后事后死亡
*/
3.3.4、守护线程
主要使用:setDaemon(true), false就是不设为守护线程
测试代码:
/**
* @Author: deemoHui
* @Description: 守护线程
* @Date Created in 2020-08-10 11:13
* @Modified By:
*/
public class MyDaemonDemo {
public static void main(String[] args) {
//线程分为守护线程和用户线程
//用户线程:当一个进程不包含任何的存活的用户线程时,进进程结束
//守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。
Thread t1 = new Thread(new MyRunnable());
//设置守护线程 true为守护线程,false就不是
t1.setDaemon(true);
// t1.setDaemon(false);
t1.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/*
main:0
Thread-0:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
Thread-0:4
main:4
Thread-0:5
*/
守护,顾名思义,就是不苟活,人在塔在,公主与骑士。
在Java 中垃圾回收线程就是特殊的守护线程。
3.3.4、线程安全
主要就是因为多线程是共享资源的,那么当多个线程对同一个资源进行修改的时候,就会出现问题。
先看线程不安全的示例:
假设火车站有三个窗口卖票,但是没有一个好的保障措施:
/**
* @Author: deemoHui
* @Description: 线程不安全,资源争抢
* @Date Created in 2020-08-10 11:33
* @Modified By:
*/
public class MyThreadSafeDemo {
public static void main(String[] args) {
//线程不安全
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (count>0){
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("卖票结束,余票:"+count);
}
}
}
}
/*
正在准备卖票
正在准备卖票
正在准备卖票
卖票结束,余票:9
正在准备卖票
卖票结束,余票:7
正在准备卖票
卖票结束,余票:8
正在准备卖票
卖票结束,余票:6
正在准备卖票
卖票结束,余票:4
正在准备卖票
卖票结束,余票:4
正在准备卖票
卖票结束,余票:3
正在准备卖票
卖票结束,余票:2
正在准备卖票
卖票结束,余票:1
正在准备卖票
卖票结束,余票:0
卖票结束,余票:-1
卖票结束,余票:-2
*/
看到了什么?居然卖出了负的。因为我们加了sleep,比如此时还剩一张票,当第一个线程进入之后,会sleep一下,这个时候count还是1,所以后边两个线程也会进入循环,那么同时三个买一张票,所以就出现了负数。这就是线程不安全,没有一个好的机制保障。
如何处理呢?怎样保证线程安全呢?
方法一:同步代码块
使用同步代码块解决,就是加一把锁,可以类比公共厕所,大家都要上厕所,每一个进去的人都把门锁上,相当于加锁,告诉别人这里有人了,排队等着。但是这个必须得是厕所门的锁,不能是自己兜里边的,要不大家都看自己兜里面的锁没锁,那直接就推门进去了,,,
格式:synchronized(锁对象)
案例演示:
/**
* @Author: deemoHui
* @Description: 线程安全
* @Date Created in 2020-08-10 11:33
* @Modified By:
*/
public class MyThreadSafeDemo {
public static void main(String[] args) {
//线程不安全
//解决方案1 同步代码块
//格式:synchronized(锁对象)
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
private final Object o = new Object();
@Override
public void run() {
//Object o = new Object(); //锁不能放在这里,这里不是同一把锁,所以锁不住
while (true) {
synchronized (o) {
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "卖票结束,余票:" + count);
} else {
break;
}
}
}
}
}
}
/*
正在准备卖票
Thread-0卖票结束,余票:9
正在准备卖票
Thread-0卖票结束,余票:8
正在准备卖票
Thread-0卖票结束,余票:7
正在准备卖票
Thread-0卖票结束,余票:6
正在准备卖票
Thread-0卖票结束,余票:5
正在准备卖票
Thread-2卖票结束,余票:4
正在准备卖票
Thread-2卖票结束,余票:3
正在准备卖票
Thread-1卖票结束,余票:2
正在准备卖票
Thread-2卖票结束,余票:1
正在准备卖票
Thread-2卖票结束,余票:0
*/
方法二:同步方法
使用同步方法解决,本质上还是加一把锁,只不过把锁加方法上。
案例代码:
/**
* @Author: deemoHui
* @Description: 线程安全2
* @Date Created in 2020-08-10 11:52
* @Modified By:
*/
public class MyThreadSafeDemo2 {
public static void main(String[] args) {
// 线程不安全
// 解决方案2 同步方法
// 线程同步synchronized
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable {
//总票数
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if (!flag) {
break;
}
}
}
public synchronized boolean sale() {
// 锁是什么:this
// 如果是静态的 就是所在的类 Ticket.class
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "卖票结束,余票:" + count);
return true;
}
return false;
}
}
}
/*
正在准备卖票
Thread-0卖票结束,余票:9
正在准备卖票
Thread-2卖票结束,余票:8
正在准备卖票
Thread-1卖票结束,余票:7
正在准备卖票
Thread-2卖票结束,余票:6
正在准备卖票
Thread-2卖票结束,余票:5
正在准备卖票
Thread-0卖票结束,余票:4
正在准备卖票
Thread-0卖票结束,余票:3
正在准备卖票
Thread-0卖票结束,余票:2
正在准备卖票
Thread-0卖票结束,余票:1
正在准备卖票
Thread-0卖票结束,余票:0
*/
方法三:使用显示锁
使用显示锁lock也可以加锁,使用它的子类ReentrantLock,值得注意的是它可以设置公平锁和非公平锁。非公平锁就是抢占式,公平锁就是排队。并且相比于前两种,这个更能体现面向对象思想,因为是自己加锁,自己解锁。
案例练习:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: deemoHui
* @Description: 线程安全3
* @Date Created in 2020-08-10 15:51
* @Modified By:
*/
public class MyThreadSafeDemo3 {
public static void main(String[] args) {
//线程不安全
//同步代码块和同步方法都属于隐式锁
//线程同步lock
//解决方案3 显示锁 Lock 子类 ReentrantLock
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
//参数为true表示公平锁 默认是false 不是公平锁
private final Lock l = new ReentrantLock(true);
@Override
public void run() {
while (true) {
l.lock();
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
l.unlock();
}else {
// break前得把锁解了,要不会卡死
l.unlock();
break;
}
}
}
}
}
/*
正在准备卖票
Thread-0卖票结束,余票:9
正在准备卖票
Thread-1卖票结束,余票:8
正在准备卖票
Thread-2卖票结束,余票:7
正在准备卖票
Thread-0卖票结束,余票:6
正在准备卖票
Thread-1卖票结束,余票:5
正在准备卖票
Thread-2卖票结束,余票:4
正在准备卖票
Thread-0卖票结束,余票:3
正在准备卖票
Thread-1卖票结束,余票:2
正在准备卖票
Thread-2卖票结束,余票:1
正在准备卖票
Thread-0卖票结束,余票:0
*/
3.4、显示锁和隐式锁的区别
3.4.1、定义
- 隐式锁(Synchronized)是Java的关键字,当它用来修饰一个方法或一个代码块时,能够保证在同一时刻最多只有一个线程执行该代码。因为当调用Synchronized修饰的代码时,并不需要显示的加锁和解锁的过程,所以称之为隐式锁。
- 显示锁(Lock)是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有的加锁和解锁操作方法都是显示的,因而称为显示锁。
3.4.2、区别
1.出身不同
-
Synchronized:Java中的关键字,是由IVM来维护的,是JVM层面的锁。
Synchronized的获取锁和释放锁是在JVM内部实现的,他就相当于给当前线程做了一个标记,由JVM内部去识别,并做出相应反应。 -
Lock:是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁。
lock是通过调用对应的API方法来获取锁和释放锁的。
2.使用方法不同
-
Synchronized是隐式锁。Lock是显示锁
-
所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。
-
我们大家都知道,在使用Synchronized关键字的时候,我们使用者根本不用写其他的代码,然后程序就能够获取锁和释放锁了。那是因为当Synchronized代码块执行完成之后,系统会自动的让程序释放占用的锁。Sync是由系统维护的,如果非逻辑问题的话,是不会出现死锁的。
-
在使用lock的时候,我们使用者需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁的现象。手动获取锁方法:lock.lock()。释放锁:unlock方法。需要配合tyr/finaly语句块来完成。
//隐式锁
synchronized(锁对象){
//任务代码
}
//显示锁
Lock l = new ReentrantLock()
@Override
public void run(){
l.lock;
//任务代码
try{
}finaly{
l.unlock;
}
}
3.等待是否可中断
- Synchronized是不可中断的。除非抛出异常或者正常运行完成
- Lock可以中断的。中断方式:
- 调用设置超时方法tryLock(long timeout ,timeUnit unit)
- 调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
4.加锁的时候是否公平
- Synchronized是非公平锁
- Lock既可以是公平锁也可以是非公平锁,它默认是非公平锁,在调用其构造方法时可以传入Boolean类型的参数,如果是true,则是公平锁;如果false,则是非公平锁。
5.能否精准唤醒
- Synchronized: 不能精准唤醒,要么随机唤醒一个线程;要么是唤醒所有等待的线程。
- Lock: 用来实现分组唤醒需要唤醒的线程,可以精确的唤醒,而不是像Synchronized那样,不能精确唤醒线程。
6.性能比较
- Synchronized是托管给JVM执行的,而Lock是Java写的控制锁的代码。
- 在Java1.5中,Synchronized是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。
- 但是到了Java1.6版本,发生了变化。Synchronized在语义上很清晰,可以进行很多优化,实现了许多以前没有的操作,Synchronized的性能这个时候并不比Lock性能差,并且官方也表示,他们更支持Synchronized,在未来的版本中还有优化的空间。
4、死锁
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
案例演示:
/**
* @Author: deemoHui
* @Description: 死锁
* @Date Created in 2020-08-10 17:03
* @Modified By:
*/
public class DeadlockDemo {
public static void main(String[] args) throws InterruptedException {
//线程死锁
Culprit c = new Culprit();
Police p = new Police();
new MyThread(c,p).start();
c.say(p);
}
static class MyThread extends Thread{
private Culprit c;
private Police p;
MyThread(Culprit c,Police p){
this.c = c;
this.p = p;
}
@Override
public void run() {
try {
p.say(c);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Culprit{
public synchronized void say(Police p) throws InterruptedException {
System.out.println("罪犯:你放了我,我放了人质");
Thread.sleep(1000);
p.fun();
}
public synchronized void fun(){
System.out.println("罪犯被放了,罪犯也放了人质");
}
}
static class Police{
public synchronized void say(Culprit c) throws InterruptedException {
System.out.println("警察:你放了人质,我放了你");
Thread.sleep(1000);
c.fun();
}
public synchronized void fun(){
System.out.println("警察救了人质,但是罪犯跑了");
}
}
}
/* 死锁
罪犯:你放了我,我放了人质
警察:你放了人质,我放了你
*/
/* 把sleep去掉,逻辑上是死锁,但是却可能因为执行速度快而正常执行,不卡死
罪犯:你放了我,我放了人质
警察救了人质,但是罪犯跑了
警察:你放了人质,我放了你
罪犯被放了,罪犯也放了人质
*/
5、多线程通信
需要用到Object类的wait()、notifyAll()方法。即当两个或多个线程相互依赖的时候,一个线程执行的时候,其他线程先休息,之后完之后把它唤醒,不然会出现错乱。
案例演示:
/**
* @Author: deemoHui
* @Description: 多线程通信
* @Date Created in 2020-08-10 17:09
* @Modified By:
*/
public class ThreadCommunicationDemo {
public static void main(String[] args) {
//多线程通信 生产者与消费者问题
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
f.setNameAndTaste("老干妈小米粥","香辣味");
}else {
f.setNameAndTaste("煎饼果子","甜辣味");
}
}
}
}
//服务员
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
private String name;
private String taste;
//true表示可以生产
boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste){
if(flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag){
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/*
服务员端走的菜的名称是:老干妈小米粥,味道是:香辣味
服务员端走的菜的名称是:煎饼果子,味道是:甜辣味
服务员端走的菜的名称是:老干妈小米粥,味道是:香辣味
服务员端走的菜的名称是:煎饼果子,味道是:甜辣味
服务员端走的菜的名称是:老干妈小米粥,味道是:香辣味
服务员端走的菜的名称是:煎饼果子,味道是:甜辣味
服务员端走的菜的名称是:老干妈小米粥,味道是:香辣味
服务员端走的菜的名称是:煎饼果子,味道是:甜辣味
服务员端走的菜的名称是:老干妈小米粥,味道是:香辣味
...
*/
6、线程的六种状态
线程可以处于以下状态之一:
- NEW
尚未启动的线程处于此状态。
- RUNNABLE
在Java虚拟机中执行的线程处于此状态。
- BLOCKED
被阻塞等待监视器锁定的线程处于此状态。
- WAITING
无限期等待另一个线程执行特定操作的线程处于此状态。
- TIMED_WAITING
正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
- TERMINATED
已退出的线程处于此状态。
线程在给定时间点只能处于一种状态。 这些状态是虚拟机状态,不反映任何操作系统线程状态。
7、Callable:创建线程的另一种方法
带返回值的线程Callable实现多线程,需要实现Callable接口和call方法,之后需要建立FutureTask。它的get方法可以使得主线程停下来等待子线程执行完返回一个结果后再执行,有什么意义呢?比如我们的主线程可能依赖于1000个子线程执行的结果,那就可以使用这个。但好像使用的不多。
案例:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author: deemoHui
* @Description: callable
* @Date Created in 2020-08-10 17:32
* @Modified By:
*/
public class MyCallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> f = new FutureTask<>(c);
Thread t = new Thread(f);
t.start();
Integer j = f.get();
System.out.println("返回值为:" + j);
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程执行" + i);
}
}
static class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程执行" + i);
}
return 100;
}
}
}
/*
子线程执行0
子线程执行1
子线程执行2
子线程执行3
子线程执行4
子线程执行5
子线程执行6
子线程执行7
子线程执行8
子线程执行9
返回值为:100
主线程执行0
主线程执行1
主线程执行2
主线程执行3
主线程执行4
主线程执行5
主线程执行6
主线程执行7
主线程执行8
主线程执行9
*/
还有一些其他的方法就不在演示了,可以参考API。
最后说一下这个Runnable 与 Callable:
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
不同点:
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出
Callable获取返回值
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执
行,如果不调用不会阻塞。
8、线程池:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
Java中有四种线程池:
- 缓存线程池
- 定长线程池
- 单线程线程池
- 周期定长线程池
8.1、缓存线程池
缓存线程池:长度无限制
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在,则创建线程并放入线程池, 然后使用
案例演示:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Author: deemoHui
* @Description: 缓存线程池
* @Date Created in 2020-08-10 17:51
* @Modified By:
*/
public class CacheThreadPool {
public static void main(String[] args) {
//线程池
//创建线程
//创建任务
//执行任务
//关闭线程
//缓存线程池
//无限制长度
//任务加入后的执行流程
//1判断线程池是否存在空闲线程 2存在则使用 3不存在则创建线程并使用
//向线程池中加入新的任务
ExecutorService service = Executors.newCachedThreadPool();
//指挥线程池执行新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.shutdown();
}
}
/*
pool-1-thread-2锄禾日当午
pool-1-thread-1锄禾日当午
pool-1-thread-3锄禾日当午
pool-1-thread-3锄禾日当午
*/
从最后可以看到,重复利用了线程3.
8.2、定长线程池
指定长度的线程池。
案例代码:
package java03.com.app.core.section5;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Author: deemoHui
* @Description: 定长线程池
* @Date Created in 2020-08-10 18:00
* @Modified By:
*/
public class FixedLengthRoutePoolDemo {
/*定长线程池
长度是指定的线程池
加入任务后的执行流程
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
**/
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.shutdown();
}
}
/*
pool-1-thread-2锄禾日当午
pool-1-thread-1锄禾日当午
pool-1-thread-2锄禾日当午
*/
可以看到只有两个线程,最后重复使用了线程2.
8.3、单线程线程池
就是长度为1的定长线程池。
案例演示:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Author: deemoHui
* @Description: 单线程线程池
* @Date Created in 2020-08-10 18:03
* @Modified By:
*/
public class MySingleThreadDemo {
/*单线程线程池
执行流程
1 判断线程池的那个线程是否空闲
2 空闲则使用
3 不空闲则等待它空闲后再使用
**/
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.shutdown();
}
}
/*
pool-1-thread-1锄禾日当午
pool-1-thread-1锄禾日当午
pool-1-thread-1锄禾日当午
*/
可以看到总共只有一个线程。
8.4、周期定长线程池
按照一定时间间隔执行的线程池。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @Author: deemoHui
* @Description: 周期定长线程池
* @Date Created in 2020-08-10 18:06
* @Modified By:
*/
public class MyScheduledThreadDemo {
/*周期任务 定长线程池
执行流程
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
周期性任务执行时
定时执行 当某个任务触发时 自动执行某任务
**/
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
//定时执行一次
//参数1:定时执行的任务
//参数2:时长数字
//参数3:2的时间单位 Timeunit的常量指定
/* scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5, TimeUnit.SECONDS); //5秒钟后执行*/
/*
周期性执行任务
参数1:任务
参数2:延迟时长数字(第一次在执行上面时间以后)
参数3:周期时长数字(没隔多久执行一次)
参数4:时长数字的单位
* **/
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5,1, TimeUnit.SECONDS);
}
}
/*
pool-1-thread-1锄禾日当午
pool-1-thread-1锄禾日当午
pool-1-thread-1锄禾日当午
pool-1-thread-2锄禾日当午
pool-1-thread-2锄禾日当午
pool-1-thread-2锄禾日当午
pool-1-thread-2锄禾日当午
pool-1-thread-2锄禾日当午
pool-1-thread-1锄禾日当午
*/
9、Lambda表达式:
简化代码的一种方法。
格式:-> + 函数的主体
/**
* @Author: deemoHui
* @Description: lambda表达式
* @Date Created in 2020-08-10 18:13
* @Modified By:
*/
public class LambdaDemo {
/*
lambda表达式
函数式编程思想
**/
public static void main(String[] args) {
//冗余的Runnable编写方式
/* Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("锄禾日当午");
}
});
t.start();*/
Thread t = new Thread(() -> System.out.println("锄禾日当午"));
t.start();
}
}
/*
锄禾日当午
*/