一、概述
在生活中多线程无处不在,有没有思考过一个问题,为什么可以同时和多个人一起打游戏?为什么聊天时双方可以同时互发消息?这些都是多线程的体现!
多任务:
举几个例子:一边吃饭一边玩手机、看电影时一边看画面一边听音频,再比如一边上厕所一边玩手机
现实中太多这样同时做多件事的例子了,看起来是多个任务同时在做,其实本质上我们的大脑在同一时间只做了一件事!只是时间交替很快,拿吃饭玩手机举例子,一刹那你在吃饭下一刻你在玩手机你觉得是同时进行的,实际上只做了一件事!
多线程的好处:
还是举几个生活中的例子,道路狭窄车多就会出现堵塞,甚至出现事故,效率会很低。为了避免这种情况,提高使用效率,于是拓宽了车道,允许多辆车同时通过。再或者原来你和你朋友共用一个游戏账号,会造成很多麻烦,但是后来你也创了一个账号这样你们就可以同时玩游戏
单线程:主线程==>执行方法==>回到主线程==>结束
多线程:主线程==>执行方法==>两个线程同时进行
二、程序、进程、线程
程序:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
进程(Process):在操作系统中运行的程序就是进程,比如QQ、游戏、IDEA等等。通俗点说进程就是程序的执行过程,他是一个动态的概念。是系统资源分配的单位
线程(Thread):进程非常大,一个进程里面可以有多个线程。举个例子:我们平时用播放器看电影,视频中同时有声音、图像、和字幕。当然一个进程至少有一个线程,否则没有存在的意义!线程是操作系统 能够进行运算、调度 的最小单位
注意:很多多线程都是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器,如果是模拟出来的多线程,即在一个CPU的情况下,在同一时间点,CPU只能执行一个代码。只是切换的很快,所以有种同时执行的错觉。就和我们边吃饭边玩手机一样,我们只是一个CPU同一时间只能干一件事!
核心概念:
- 线程是独立的执行路径,main线程和jc线程(垃圾处理线程)各走各的互不影响
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如main线程、jc线程
- main称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由CPU安排调度,调度器是和操作系统紧密相关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制
- 线程会带来额外的开销,如CPU调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
三、线程的创建
线程的创建有以下三种方式
1.继承Thread类
查看jdk帮助文档,发现他实现了Runnable接口,即线程的第二种创建方式。
使用方法:
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
public class TestThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("学习多线程的第"+i+"天");
}
}
public static void main(String[] args) {
TestThread1 testThread1 = new TestThread1();
//这里注意调用start()开启线程,调用run()则按顺序执行!
testThread1.start();
for (int i = 0; i < 200; i++) {
System.out.println("学习Java的第"+i+"天");
}
}
}
注意:线程开启不一定立即执行,而是由CPU调度执行!
案例:下载网图
这里需要导入一个jar包,apache下的commons.io这个包(官网自行下载),将它沾到IDEA中,右键点击Add as Library。
这个包下有FileUtils工具类,使用copyURLToFile(URL source, File destination)方法拷贝url到一个文件。
public class TestThread extends Thread{
private String url;
private String name;
//使用有参构造获取copyURLToFile所需的参数
public TestThread(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
DownLoad downLoad = new DownLoad(url,name);
System.out.println("下载的文件名为:"+name);
}
public static void main(String[] args) {
TestThread testThread = new TestThread("图片url","图片名字");
TestThread testThread1 = new TestThread("图片url","图片名字");
TestThread testThread2 = new TestThread("图片url","图片名字");
testThread.start();
testThread1.start();
testThread2.start();
}
}
class DownLoad{
public DownLoad(String url,String name) {
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.实现 Runnable 接口
使用方法:
- 自定义线程类实现Runnable接口
- 重写run()方法,编写线程执行体
- 创建线程对象
- 将线程对象在创建Thread时作为参数传递,并启动
public class TestThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("第"+i+"天学习多线程");
}
}
public static void main(String[] args) {
TestThread2 testThread2 = new TestThread2();
// Thread thread = new Thread(testThread2);
// thread.start();
new Thread(testThread2).start();
for (int i = 0; i < 200; i++) {
System.out.println("第"+i+"天学习Java");
}
}
}
解释new Thread(testThread2).start()写的原因:无论我们以怎样的形式实现多线程,都需要调用Thread类中的start方法去向操作系统请求io,cup等资源,Tread中有一个构造方法是传入一个Runnable接口类型的参数来启动线程。
推荐使用实现Runnable接口!
原因:
- 继承有单继承的局限性,而接口可以多继承
- 实现Runnable接口启动线程是讲线程对象传入Thread构造方法中,灵活方便,方便同一个对象被多个线程使用
典型案例龟兔赛跑:
sleep(long millis)
当前正在执行的线程休眠(暂停执行)指定的毫秒数
public class Race implements Runnable {
private static String winner;//winnerֻ胜利者 使用static的原因:胜利者只有一个
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (Thread.currentThread().getName().equals("兔子") && i==50){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
boolean flag = gameOver(i);
if (flag){
break;
}
System.out.println(Thread.currentThread().getName() + "==>跑了" + i + "步");
}
}
private boolean gameOver(int steps) {
if (winner != null) {
return true;
} else {
if (steps == 100) {
winner = Thread.currentThread().getName();
System.out.println(winner+"获胜了");
return true;
}
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();
}
}
3.实现Callable接口
无论以上哪种方式实现多线程,都存在一个问题,就是线程run方法没有返回值,如果一个线程需要有返回值时,可以采用实现Callable接口来实现多线程!
首先看一下Callable接口的源码
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
首先@FunctionalInterface定义了这个接口为函数式接口,Callable接口接受一个泛型作为接口中call方法的返回值类型,因此我们在使用时需要传入一个返回值类型。
使用方法1:
由于Thread
类中并没有定义任何构造方法可以直接接收Callable接口对象实例,并且需要接收call()方法返回值的问题,所以从JDK1.5开始提供有一个java.until.concurrent.FutureTask类。
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
可以看出FutureTask类也是实现了Runnable接口的
FutureTask类的常用方法:
方法 | 类型 |
---|---|
public FutureTask(Callable callable) | 构造方法 |
public FutureTask(Runnable runnable, V result) | 构造方法 |
public V get() throws InterruptedException, ExecutionException | 普通方法 |
从第一个构造方法可以看出,FutureTask可以传入一个Callable接口类型的参数,而FutureTask实现了RunnableFuture,而 RunnableFuture 接口又继承了Runnable接口,因此我们可以将FutureTask 的一个实例当做是一个Runnable接口的实例传入Thread来启动我们新建的线程。
public class TestCallable implements Callable {
@Override
public Boolean call() {
for (int i = 0; i < 11; i++) {
System.out.println("java好难");
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 0; i < 11; i++) {
System.out.println("学习多线程"+i);
TestCallable testCallable = new TestCallable();
FutureTask<Boolean> task = new FutureTask<Boolean>(testCallable);
new Thread(task).start();
//Boolean flag = task.get();
//System.out.println(flag);
}
}
}
使用方法2:
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(线程的数量);
- 提交执行:Future result1 =ser.submit(目标对象);
- 获取结果:boolean rs = result1.get();
- 关闭服务:ser.shutdown();
ExecutorService 和 Executors会在下面线程池中讲
public class TestCallable implements Callable {
@Override
public Boolean call() {
for (int i = 0; i < 11; i++) {
System.out.println("java好难");
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 0; i < 11; i++) {
System.out.println("学习Callable接口"+i);
TestCallable testCallable = new TestCallable();
//创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(2);
//提交执行
Future<Boolean> result1 =ser.submit(testCallable);
//获取结果
boolean rs = result1.get();
//关闭服务
ser.shutdown();
}
}
}
四、线程的状态(五大状态)
线程有五大状态:创建 就绪 阻塞 运行 死亡
创建状态:Thread r = new Thread();线程对象一旦创建就进入了创建状态
就绪状态:当调用start()方法,线程立刻进入就绪状态,但不意味着立刻执行调度而是等待CPU调度
运行状态:CPU调度后进入运行状态,线程才真正执行线程体的代码块
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
死亡状态:线程中断或者结束,一旦进入死亡状态就不能继续启动
线程方法:
方法 | 说明 |
---|---|
setPriority(int newPriority) | 设置线程的优先级 |
static void sleep(long millis) | 让正在执行的线程休眠(暂停执行)指定的毫秒数 |
void join() | 等待该线程死亡 |
static void yield() | 暂停正在执行的线程对象 |
void interrupt() | 中断这个线程,不建议使用这个方法! |
boolean isAlive() | 测试当前线程是否已被中断 |
Thread.State getState() | 得到当前线程的状态 |
停止线程
不推荐使用JDK提供的 stop()、 destroy()方法。已废弃!
推荐线程自己停下来!
怎么实现呢?
使用一个标志位进行终止变量 当flag=false,则终止线程运行
public class TestTreadStop implements Runnable{
int i=0;
boolean flag = true;
@Override
public void run() {
while (flag){
i++;
System.out.println("子线程执行了"+i+"次");
}
}
//自己定义的停止方法非Thread类的
private void stop(){
flag = false;
}
public static void main(String[] args) {
TestTreadStop testTreadStop = new TestTreadStop();
new Thread(testTreadStop).start();
for (int i = 0; i < 300; i++) {
if (i==100){
testTreadStop.stop();
System.out.println("子线程该停止了!");
}
System.out.println("主线程执行了"+i+"次");
}
}
}
主线程和子线程共同抢占CPU,两者交替执行,当主线程的i==100时,调用了子线程的stop方法,那么子线程停止就不再执行,那么此时程序中就存在一个主线程在执行,直到停止。
线程休眠(sleep)
public static native void sleep(long millis) throws InterruptedException;
millis 为毫秒数,指定线程停止的毫秒数。
存在异常 InterruptedException
注意: 每一个对象都有一个锁,sleep不会释放锁;
sleep的使用:
1、模拟网络延时
还是用抢票这个例子
public class TestThread3 implements Runnable{
private int num = 10;
@Override
public void run() {
while (true){
if (num<=0){
break;
}
/* try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
System.out.println(Thread.currentThread().getName()+"==>拿到了第"+num--+"张票");
}
}
public static void main(String[] args) {
TestThread3 thread3 = new TestThread3();
new Thread(thread3,"小米").start();
new Thread(thread3,"大米").start();
new Thread(thread3,"黄牛").start();
}
}
不加sleep方法,运行发现正常抢票
当我们增加了网络延时后,会发现很多问题!
以上例子通过三个线程抢票,模拟网络延迟,放大了问题的发生性,我们知道当我们定义多个线程共同抢夺同一个资源时,也没有使用锁是不安全的(两个人拿到同一张票),我们不模拟延时,很难发现问题。而当我们使用了sleep后,我们很容易看到问题发生的现状(两个人抢到了同一张票或者抢到一张不存在的票!)。
2、实现倒计时
public class TestSleep {
private void tenDown(){
int number = 10;
try {
while (true){
System.out.println(number--);
if (number<=0){
break;
}
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TestSleep().tenDown();
}
}
3、控制台输出当前时间
public class TestSleep2 {
public static void main(String[] args) {
Date date = new Date();
try {
while (true){
System.out.println(new SimpleDateFormat("YYYY/MM/DD").format(date));
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
Thread.sleep(1000);
date = new Date();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程礼让(yield)
礼让线程 让当前正在执行的线程暂停但不阻塞,是从运行状态返回到就绪状态
让cpu重新调度,礼让不一定成功!看CPU心情
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行");
Thread.yield();//礼让
System.out.println(Thread.currentThread().getName()+"停止");
}
}
礼让成功:
a执行
b执行
b停止
a停止
礼让失败:
b执行
b停止
a执行
a停止
a,b两个线程,假设a线程进入了CPU正在执行,a礼让了b,两个同时处于就绪状态,等待CPU调度!可能礼让不成功,又是调度了a
线程插队(join)
Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
join可以理解为插队,比如你在排队办理一些业务时,前面有个人插队说自己是vip可以优先办理,其他人只能等着他办理完才能继续。线程也是如此!使用了join方法(其他线程发生了阻塞,停止执行),直到插队线程执行完毕
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("我是VIP"+i);
}
}
public static void main(String[] args) {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
for (int i = 0; i < 500; i++) {
if (i==200){
try {
thread.start();
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("路人"+i);
}
}
}
当i==200时路人线程要等待VIP全部执行完才继续执行
线程状态
可以使用getState()获得当前线程状态
public class TestSta {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for (int i = 0; i < 2; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread.State state = thread.getState();
System.out.println(state);
thread.start();
state = thread.getState();
System.out.println(state);
while (state != Thread.State.TERMINATED){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
state = thread.getState();
System.out.println(state);
}
}
}
结果:
NEW
RUNNABLE
TIMED_WAITING
.
.这里省略多个TIMED_WAITING
.
TERMINATED
线程优先级
需要用到的方法
- public final int getPriority() 获取优先级
- public final void setPriority(int newPriority) 设置优先级
每个线程都有默认的优先级。主线程的默认优先级 Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
注意:优先级高的只是执行的概率更高了,并不代表一定会先执行优先级高的!看CPU的调度。线程属性的设置要在线程启动之前!
查看Thread类源码发现线程的最小优先级为1,最大优先级为10,默认优先级为5。如果超过最大或者低于最小会抛 IllegalArgumentException 异常
线程分为 用户线程(例如:main;虚拟机必须确保用户线程执行完毕) 和守护线程(例如:gc;虚拟机不用等待守护线程执行完毕)
守护线程与用户线程
守护线程:是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程
用户线程:可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。如我们使用 Thread 创建的线程在默认情况下都属于用户线程
怎么使线程成为用户线程与守护线程?
- 通过
Thread.setDaemon(false)
设置为用户线程 - 通过
Thread.setDaemon(true)
设置为守护线程 - 如果不设置线程属性,那么默认为用户线程
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread t1 = new Thread(god);
t1.setDaemon(true);
t1.start();
Thread t2 = new Thread(you);
t2.start();
}
}
//守护线程
class God implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("祝你开心幸福"+i+"天");
}
}
}
//用户线程
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Happy every day");
}
System.out.println("goodBye world");
}
}
运行发现:用户线程全部执行,守护线程只执行了一部分。
这是因为 虚拟机必须确保用户线程执行完毕,而不用等待守护线程执行完毕 。
上面代码还会出现:当用户线程死后,我们看到守护线程还在执行一段时间,原因是当我们销毁用户线程后也需要一段时间,因为cpu切换速度比较快,所以我们的守护线程也会运行一段时间。
守护线程的作用:如,后台记录操作日志,监控内存,垃圾回收等待…
五、线程同步
当多个线程同时操作同一个对象时会发生并发问题!
并行和并发
在学习多线程时,常常会遇到并行和并发两个名词,它们看起来是一个概念,实际上它们是有区别的!
举个例子理解一下:拿学校食堂打饭来说,并发就是只有一个窗口排了两队人,这些人交替打饭。而并行是两个窗口两队人,分别在两个窗口打饭。
并发(多线程操作同一个资源):CPU一核,模拟出多条线程,天下武功,唯快不破,快速交替
下图展示了两个任务并发执行的过程(交替执行):
并行:并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。
同样是执行两个任务,双核 CPU 的工作状态(也就是并行的执行过程)如下图所示:
并发+并行
执行任务的数量恰好等于 CPU 核心的数量,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,尤其是电脑和手机,开机就几十个任务,而 CPU 往往只有 4 核、8 核或者 16 核,远低于任务的数量,这个时候就会同时存在并发和并行两种情况:所有核心都要并行工作,并且每个核心还要并发工作。
例如一个双核 CPU 要执行四个任务,它的工作状态如下图所示:
每个核心并发执行两个任务,两个核心并行的话就能执行四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,这跟操作系统的分配方式,以及每个任务的工作状态有关系。
注意:
单核 CPU 只能并发,无法并行;换句话说,并行只可能发生在多核 CPU 中。
在多核 CPU 中,并发和并行一般都会同时存在。
线程同步
- 现实生活中,我们会遇到 ” 同一个资源 , 多个人都想使用 ” 的问题 , 比如,食堂排队 打饭 , 每个人都想吃饭 , 最天然的解决办法就是 , 排队 . 一个个来.
举个例子:购买火车票的过程 - 处理多线程问题时 , 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 . 这时候我们就需要线程同步 . 线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个对象的等待池 形成队列, 等待前面线程使用完毕 , 下一个线程再使用
- 由于同一进程的多个线程共享同一块存储空间 , 在带来方便的同时,也带来了访问 冲突问题 , 为了保证数据在方法中被访问时的正确性 , 在访问时加入 锁机制
synchronized , 当一个线程获得对象的排它锁 , 独占资源 , 其他线程必须等待 , 使用后释放锁即可 . 存在以下问题 :
- 一个线程持有锁会导致其他所有需要此锁的线程挂起 ;
- 在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题 ;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置 , 引起性能问题
线程同步需要 队列+锁
队列可以看作是排队。锁可以看作是排队上厕所,每个人进入厕所都会锁上门,然后解决完后打开锁,下一个人进去。这样使得线程变得安全,但是性能降低!
同步方法
synchronized实现原理就是 队列和锁
同步块
- 同步块:synchronized(obj){ }
- obj称之为 同步监视器
- obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是this , 就是这个对象本身 , 或者是 class
- 同步监视器的执行过程
- 第一个线程访问 , 锁定同步监视器 , 执行其中代码 .
- 第二个线程访问 , 发现同步监视器被锁定 , 无法访问 .
- 第一个线程访问完毕 , 解锁同步监视器 .
- 第二个线程访问, 发现同步监视器没有锁 , 然后锁定并访问
同步方法的弊端
方法里面有只读和需要修改的代码,只读的代码可以多人同时访问,而修改代码不允许同时进行修改。这时就不建议使用同步方法,方法里需要修改的内容才需要锁,锁的太多会造成资源浪费性能降低!
代码示例
下面举几个线程不安全的例子,并通过同步方法和同步代码块解决
买票
这里多个线程资源抢占票,若不适用锁机制不同对象会抢到同一张票,还会造成票的负数,显然这是线程不安全的,那么我们使用synchronized方法实现线程安全
public class Unsafe1 {
public static void main(String[] args) {
station station = new station();
Thread t1 = new Thread(station,"张三");
Thread t2 = new Thread(station,"李四");
Thread t3 = new Thread(station,"黄牛");
t1.start();
t2.start();
t3.start();
}
}
class station implements Runnable{
private int ticket = 5;
boolean flag = true;
@Override
public void run() {
while (flag){
buy();
}
}
private void buy(){
if (ticket <= 0){
flag = false;
return;
}
try {
//增大事情的发生性
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"获得了第"+ticket--+"张票");
}
}
运行结果发现:同时抢到了一张票,现实生活中是不允许的!
解决:在buy方法前加修饰符synchronized,其他不变,实现队列加锁
private synchronized void buy(){
if (ticket <= 0){
flag = false;
return;
}
分析:我们是对票进行操作,票属于station 这个类,而同步方法的同步监视器就是this , 就是这个对象本身!相当于把票放在一个房间里,来一个线程进房间上锁取走票后解锁,下一个继续这样的操作。
取钱
银行卡上有100万,你和你女朋友同时取钱,你取50万,女朋友取100万,如果不上锁不实现同步,账户将会出现负的50万(程序比较笨)
解决:
public class Unsafe2 {
public static void main(String[] args) {
Count count = new Count(100,"购房基金");
bank zs = new bank(count,50,"你");
bank ls = new bank(count,100,"女朋友");
zs.start();
ls.start();
}
}
class Count {
int money;
String name;
public Count(int money, String name) {
this.money = money;
this.name = name;
}
}
class bank extends Thread {
Count count;
int drawingMoney;
int nowMoney;
public bank(Count count,int drawingMoney,String name){
super(name);
this.count = count;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
//取钱
//synchronized默认锁this
synchronized (count){
if (count.money-drawingMoney < 0){
System.out.println(this.getName()+"余额不足,取款未成功");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.money = count.money-drawingMoney;
nowMoney = nowMoney+drawingMoney;
System.out.println(count.name+"取走了"+count.money);
System.out.println(this.getName()+"拥有"+nowMoney);
}
}
}
这里解释一下为什么使用同步块而不是锁方法:因为同步方法默认锁的是this,也就是本类(bank),而实际改变的是账户count,bank并没有改变。
ArrayList
ArrayList是线程不安全的,通过一个例子了解为什么说是不安全的
public class Unsafe3 {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
//Thread.sleep(1000);
System.out.println(list.size());
}
}
首先我们不加延时,发现输出的大小不为10000,你可能要说因为main线程跑完了,list这个线程还没有完,造成了结果不对。那我们加上延时多次运行发现还是有时候他不是10000。说明是不安全的!同一时间两个线程同时操作了一个位置,造成了数据的覆盖!
解决:在lambda表达式中加同步代码块
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
CopyOnWriteArrayList
讲完线程不安全的,我们来了解一下线程安全的
CopyOnWriteArrayList 是JUC下的一个方法,在java.util.concurrent.CopyOnWriteArrayList包下
public class TestCopyOnWriteArrayList {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
Lock锁
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock 接口 是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- 使用Lock锁通常使用它的实现类 ReentrantLock(可重入锁),ReentrantLock 类实现了 Lock,可以显式加锁、释放锁
用reentrantLock.lock();来加锁
reentrantLock.unlock(); 释放锁或者说关锁
我们在CopyOnWriteArrayList类的源码中也可以发现这个类
还是以买票为例
public class TestLock {
public static void main(String[] args) {
sta sta = new sta();
Thread t1 = new Thread(sta,"张三");
Thread t2 = new Thread(sta,"李四");
Thread t3 = new Thread(sta,"黄牛");
t1.start();
t2.start();
t3.start();
}
}
class sta implements Runnable{
private int ticket = 10;
boolean flag = true;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (flag){
buy();
}
}
private void buy(){
try {
Thread.sleep(400);
//加锁
lock.lock();
if (ticket <= 0){
flag = false;
return;
}
System.out.println(Thread.currentThread().getName()+"获得了第"+ticket--+"张票");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放锁
lock.unlock();
}
}
}
synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,不要忘记关锁!!!),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,jvm将花费较短时间来调度线程,性能更好。并且具有更好的扩展性(有更多的子类)
- 优先使用顺序:
- Lock>同步代码块>同步方法
六、死锁
死锁:多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源,而导致两个线程或者多个线程都在等待对方释放资源,都停止执行的情形。若无外力作用他们都无法进行下去。
死锁形成条件:某一个同步块同时拥有两个以上对象的锁时,就可能发生“死锁问题”。也就是两个线程在各自拥有锁的情况下,又去尝试获取对方的锁,从而造成的一直阻塞的情况。
举个简单的例子:有A、B两个小孩,A小孩拥有一辆玩具车,B小孩拥有一把玩具枪,他们都想要对方手里的玩具但是不肯交换。这时就造成了死锁!
代码演示
a,b两个女孩抢 口红和镜子两个资源,一个拥有镜子想要口红,一个拥有口红想要镜子
public class DeadLock {
public static void main(String[] args) {
MakeUp g1 = new MakeUp(0, "a");
MakeUp g2 = new MakeUp(1, "b");
g1.start();
g2.start();
}
}
class Lipstick {
}
class Mirror {
}
class MakeUp extends Thread {
//static保证资源只有一份(只运行一次)
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;
String girlName;
public MakeUp(int choice, String girlName) {
super(girlName);
this.choice = choice;
}
@Override
public void run() {
if (choice == 0) {
synchronized (lipstick) {
System.out.println(Thread.currentThread().getName() + "拿到了" + "口红");
synchronized (mirror) {
System.out.println(Thread.currentThread().getName() + "拿到了" + "镜子");
}
}
}else {
synchronized (mirror) {
System.out.println(Thread.currentThread().getName() + "拿到了" + "镜子");
synchronized (lipstick) {
System.out.println(Thread.currentThread().getName() + "拿到了" + "口红");
}
}
}
}
}
程序卡在这里,不停止。
解决:当互相想要对方手里的资源时,双方都释放锁。也就是互相交换!(获得了一把锁之后不再去请求获取另一把锁)
@Override
public void run() {
if (choice == 0) {
synchronized (lipstick) {
System.out.println(Thread.currentThread().getName() + "拿到了" + "口红");
}
synchronized (mirror) {
System.out.println(Thread.currentThread().getName() + "拿到了" + "镜子");
}
}else {
synchronized (mirror) {
System.out.println(Thread.currentThread().getName() + "拿到了" + "镜子");
}
synchronized (lipstick) {
System.out.println(Thread.currentThread().getName() + "拿到了" + "口红");
}
}
}
现在的执行流程:
- a拿到口红,b拿到镜子
- a释放口红,b释放镜子
- a拿到镜子,b拿到口红
- 然后都释放程序结束
死锁产生的原因:
产生死锁的四个必要条件:
① 互斥条件:一个资源只能被一个线程占有,当这个资源被占用后其他线程就只能等待。
②不可剥夺条件:当一个线程不主动释放资源时,此资源一直被拥有线程占有。
③请求并持有条件:线程已经拥有一个资源后仍然不满足,又尝试请求新的资源。
④环路等待条件:产生死锁一定是发生了线程资源环路链。
改变死锁中的任意一个或多个条件就可以解决死锁问题!
七、线程协作(生产者消费者模式)
线程通讯
- 应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品 , 生产者将生产出来的产品放入仓库 , 消费者将仓库中产品取走消费
- 如果仓库中没有产品 , 则生产者将产品放入仓库 , 否则停止生产并等待 , 直到仓库中的产品被消费者取走为止
- 如果仓库中放有产品 , 则消费者可以将产品取走消费 , 否则停止消费并等待 ,直到仓库中再次放入产品为止
这是一个线程同步问题 , 生产者和消费者共享同一个资源 , 并且生产者和消费者之 间相互依赖 , 互为条件 .
- 对于生产者 , 没有生产产品之前 , 要通知消费者等待 . 而生产了产品之后 , 又需要马上通知消费者消费
- 对于消费者 , 在消费之后 , 要通知生产者已经结束消费 , 需要生产新的产品以供消费.
- 产者消费者问题中 , 仅有synchronized是不够的
- synchronized可阻止并发更新同一个共享资源 , 实现了同步
- synchronized不能用来实现不同线程之间的消息传递 (通信)
Java提供了几个方法解决线程之间的通信问题!
均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException
生产者消费者问题
管程法:
生产者 : 负责生产数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;
消费者 : 负责处理数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;
缓冲区 : 消费者不能直接使用生产者的数据 , 他们之间有个 “ 缓冲区
生产者将生产好的数据放入缓冲区 , 消费者从缓冲区拿出数据
代码示例:
//管程法
public class TestPC {
public static void main(String[] args) {
Cache cache = new Cache();
Producer producer = new Producer(cache);
Customer customer = new Customer(cache);
producer.start();
customer.start();
}
}
//生产者
class Producer extends Thread{
Cache cache ;
public Producer(Cache cache){
this.cache = cache;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生产了"+i+"只鸡");
cache.produce(new Foods(i));
}
}
}
//消费者
class Customer extends Thread{
Cache cache ;
public Customer(Cache cache){
this.cache = cache;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了第"+cache.buy().id+"只鸡");
}
}
}
class Foods{
int id;
public Foods(int id){
this.id = id;
}
}
//缓冲区
class Cache{
Foods[] foods = new Foods[10];
int count = 0;
public synchronized void produce(Foods food){
while (count == foods.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
foods[count] = food;
count++;
//缓冲区有food了即可通知消费者消费
this.notifyAll();
}
public synchronized Foods buy(){
while (count == 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
Foods food = foods[count];
this.notifyAll();
return food;
}
}
信号灯法
就像日常生活中的红绿灯,红灯停绿灯行。当flag=true时生产者生产,消费者等待,flag=false时消费者消费,生产者等待
//信号灯法
public class TestPC2 {
public static void main(String[] args) {
Tv tv = new Tv();
Player player = new Player(tv);
Watcher watcher = new Watcher(tv);
player.start();
watcher.start();
}
}
//生产者
class Player extends Thread{
Tv tv;
public Player(Tv tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i%2==0){
this.tv.show("魔术");
}else {
this.tv.show("科学");
}
}
}
}
//消费者
class Watcher extends Thread{
Tv tv;
public Watcher(Tv tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.tv.watch();
}
}
}
//产品
class Tv{
String play;
boolean flag =true;
public synchronized void show(String play){
while (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("表演者表演"+play);
this.notifyAll();
this.play = play;
this.flag = !this.flag;
}
public synchronized void watch(){
while (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众收看"+play);
this.notifyAll();
this.flag = !this.flag;
}
}
注意:对于生产者和消费者问题,最好不要用if判断,用if判断的话,唤醒后线程会从wait之后的代码开始运行,不会重新判断if条件,直接继续运行if代码块之后的代码,多个线程中会出现脏读的情况,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
if判断的叫虚假唤醒。
线程池法
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。(使用线程池可以提高性能!)
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
- 可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
线程池的好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理(…)
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService : 真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task);执行任务,有返回值,一般又来执行Callable
- void shutdown() :关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
public class TestPool {
public static void main(String[] args) {
//创建线程池 后面参数为线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
八、补充
transient和volatile关键字是多线程看源码时见过很多所以补充一下
transient关键字
transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。
想了解关于序列化与反序列化的知识点这里
为什么要使用transient关键字
在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。
transient使用总结
- 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法被访问。
- transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
- transient一般在实现了Serializable接口的类中使用。
- 一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。
volatile关键字
问:请谈谈你对volatile的理解?
答:volatile是Java虚拟机提供的轻量级的同步机制,它有3个特性:
1)保证可见性
2)不保证原子性
3)禁止指令重排