1、线程简介
想学习线程的友友建议可以用b站看狂神的视频,博主大力推荐;
视频地址:https://www.bilibili.com/video/BV1V4411p7EF?p=1&vd_source=baa7aeee4e9189a4f5dfc295165034fe
觉得视频不错可以三连关注走一波嗷!不要忘记给狂神支持!
进程、线程、多线程;
进程
进程就是执行程序的一次执行过程,一个程序执行时,便是启动了一个进程,进程是系统分配资源的单位;
进程是一个动态的概念,程序本身并不是进程,而在操作系统中运行的程序被叫做进程!!!
线程
一个进程中包含了若干个线程,线程是CPU调度和执行的单位;
多线程
多线程就是指多个线程;
2、线程创建
2.1、线程的创建方式(三种)
一说有四种,最后一种说是线程池,但其实也是通过通过实现Runnable或者Callable接口来实现的,究其底层其实也就是实现重写run方法;
1、继承Thread类
-
自定义线程类继承Thread类;
-
重写run()方法,编写线程执行体;
-
创建线程对象,调用start()方法启动线程;
package com.newThread.demo01;
/**
* 线程交替执行,看CPU的调度处理
* 一定得记得调用start方法
*/
public class TestThread1 extends Thread{
// 重写 run 方法
@Override
public void run(){
for (int i = 0; i < 200; i++) {
System.out.println("多线程交替执行测试----"+ i );
}
}
public static void main(String[] args) {
// new 一个线程
TestThread1 testThread1 = new TestThread1();
// 调用新建的线程
testThread1.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程执行一次-----" + i);
}
}
}
测试结果:
案例(网图下载)
通过 commons-io 的jar包,下载网上的图片(通过URL);
(1)导入下载的包;
通过Maven调用 commons-io 的包;
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
(2)编写下载器以及重写thread类;
package com.zeiyalo.demo01;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* @author Zeiyalo
*/
public class TestThread2 extends Thread{
private String url;
private String name;
public TestThread2(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run(){
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(this.url, this.name);
System.out.println("图片下载完成!");
}
}
/**
* 下载器
*/
class WebDownloader{
public void downloader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}
(3)编写测试类;
import com.zeiyalo.demo01.TestThread2;
import org.junit.Test;
public class MyTest {
@Test
public void TestThread2(){
TestThread2 t1 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Active.png","stared.png");
TestThread2 t2 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Black.png","star.png");
t1.start();
t2.start();
}
}
(4)测试结果;
选了CSDN上的两中点赞图标进行测试;
并不能使用测试类进行下载,控制台未提示我图片下载完成,只提示测试通过;
所以改用psvm主程序调用方式进行改进,完成下载;
import com.zeiyalo.demo01.TestThread2;
import org.junit.Test;
public class MyTest {
// 测试失败
@Test
public void TestThread2(){
TestThread2 t1 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Active.png","stared.png");
TestThread2 t2 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Black.png","star.png");
t1.start();
t2.start();
}
// 使用主程序调用,成功
public static void main(String[] args) {
TestThread2 t1 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Active.png","stared.png");
TestThread2 t2 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Black.png","star.png");
t1.start();
t2.start();
}
}
案例完成;
2、实现Runnable接口;
package com.zeiyalo.demo01;
/**
* @author Zeiyalo
*/
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("多线程交替执行测试----"+ i );
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程执行一次-----" + i);
}
}
}
实现了runnable接口后一般使用一个线程来调用runnable接口的实现类;
案例(卖票、卖票实现,会遇到并发抢资源问题,可以通过加锁解决!)
设计一个卖票线程,然后几个线程去抢票;
package com.zeiyalo.demo01;
/**
* @author Zeiyalo
*/
public class TicketRunnable implements Runnable {
// 票数
private int num = 10;
@Override
public void run() {
while (true) {
if (num <= 0) {
break;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + num + "张票!");
num--;
}
}
public static void main(String[] args) {
// 卖票线程
TicketRunnable runnable = new TicketRunnable();
// 多个抢票线程
new Thread(runnable, "小明").start();
new Thread(runnable, "大红").start();
new Thread(runnable, "梁江").start();
new Thread(runnable, "余华").start();
}
/**
* 测试结果:
* 小明抢到了第10张票!
* 余华抢到了第10张票!
* 余华抢到了第8张票!
* 余华抢到了第7张票!
* 余华抢到了第6张票!
* 余华抢到了第5张票!
* 余华抢到了第4张票!
* 大红抢到了第10张票!
* 梁江抢到了第10张票!
* 梁江抢到了第1张票!
* 大红抢到了第2张票!
* 余华抢到了第3张票!
* 小明抢到了第9张票!
*
* 上面的例子中,由于卖票线程未加锁,所以会导致一张票被多个其他线程抢到,这是一个多线程并发问题,可以通过加锁来实现对资源的更 * 好的分配;
*/
}
案例完成,出现并发问题;
小结:
- 不建议使用继承Thread类方法,因为OPP单继承的局限性;
- 建议使用实现Runnable接口方法,可以避免单继承的局限性,比较灵活方便,同一个对象可以被多个线程使用;
- 两者都具有多线程能力,但是第一种(继承)的启动线程的方法是"子类对象.start()“,第二种方法(实现接口)的启动方法是"传入目标对象(接口实现类) + new Thread(目标对象).start()”;
案例(龟兔赛跑)
乌龟和兔子赛跑,兔子中间睡觉导致乌龟赢得比赛;
解决思路:
希望自己设计一个参赛选手线程,两个参赛选手赛程来争抢比赛距离线程,但暂时知识还不够实现;
狂神说的思路有问题,因为他的思路中,兔子和乌龟是一人走一步,最终两者相加为一百,所以说思路错了,应该是各自走一段相等距离,谁先走到谁算赢;
这个问题的本质就是锻炼使用 sleep() 方法,不必过多纠结;
3、实现Callable接口(了解);
通过之前实现的图片下载类来进行改造;
实现步骤
(1)实现Callable接口;
public class MyCallable implements Callable<Boolean> {
(2)重写call方法;
@Override
public Boolean call() throws Exception {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(this.url, this.name);
System.out.println("图片 " + Thread.currentThread().getName() + " 下载完成!");
return true;
}
(3)创建执行服务;
// 创建执行服务
ExecutorService service = newFixedThreadPool(2);
(4)提交执行;
// 提交执行
Future<Boolean> r1 = service.submit(c1);
Future<Boolean> r2 = service.submit(c2);
(5)获取结果;
// 获取结果
System.out.println(r1.get());
System.out.println(r2.get());
(6)关闭服务;
// 关闭服务
service.shutdown();
最终代码
package com.zeiyalo.demo02;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import static java.util.concurrent.Executors.*;
/**
* @author Zeiyalo
*/
public class MyCallable implements Callable<Boolean> {
private String url;
private String name;
public MyCallable(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public Boolean call() throws Exception {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(this.url, this.name);
System.out.println("图片 " + Thread.currentThread().getName() + " 下载完成!");
return true;
}
public static void main(String[] args) {
MyCallable c1 = new MyCallable("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Active.png","stared.png");
MyCallable c2 = new MyCallable("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Black.png","star.png");
// 创建执行服务
ExecutorService service = newFixedThreadPool(2);
// 提交执行
Future<Boolean> r1 = service.submit(c1);
Future<Boolean> r2 = service.submit(c2);
try {
// 获取结果
System.out.println(r1.get());
System.out.println(r2.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
// 关闭服务
service.shutdown();
}
}
}
/**
* 下载器
*/
class WebDownloader{
public void downloader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}
测试结果如下:
额外探究图片名称
在原来的call方法中加上这样一句话,两者是否有区别;
测试结果:
说明getName() 的取值在有自己的变量name时仍取线程池中的名;
Callable与前两种方式的对比
- 使用前需要开启线程池;
- 需要使用服务来提交服务执行;
- 有返回值;
- 使用完需要关闭服务;
3、静态代理
案例(结婚案例)
你结婚时,找婚庆公司,婚庆公司就是一个代理!
package com.zeiyalo.proxy;
/**
* 静态代理:
* 1、将一些琐事交给外层的代理对象去处理,自己本身的真实对象只需要专注与最重要的事就够了;
* 2、在线程中也用到了类似的思想,处理对应业务的线程只需要去关注于业务,其他的琐事交给代理对象也就是外层线程处理就好了;
* 3、其实也有着加一层和AOP的思想在其中;
*/
/**
* @author Zeiyalo
*/
public class StaticProxy {
public static void main(String[] args) {
new WeddingCorp(new You()).happyMarry();
}
}
interface Marry{
void happyMarry();
}
/**
* 真实结婚对象
*/
class You implements Marry{
@Override
public void happyMarry() {
System.out.println("结婚ing,很开心!");
}
}
/**
* 婚庆公司,代理对象
*/
class WeddingCorp implements Marry{
private Marry targetObj;
public WeddingCorp(Marry targetObj) {
this.targetObj = targetObj;
}
@Override
public void happyMarry() {
before();
targetObj.happyMarry();
after();
}
private void after() {
System.out.println("婚礼结束,收尾款!!");
}
private void before() {
System.out.println("婚礼准备,筹划,准备举办!!");
}
}
总结:
- 真实对象和代理对象都要实现一个接口;
- 代理对象要代理真实角色,需要真实对象作为一个属性参数;
- Runnable接口的实现就是类似与静态代理;
4、Lambda表达式
为了避免匿名内部类定义过多,于是Java推出了Lambda表达式(在Java 8之后才能使用),实质上属于是函数式编程的概念;
一般来说有三种表达形式:
- (params) -> expression[表达式]
- (params) -> statement[语句]
- (params) -> { statement }
在线程提到这个是因为可以直接使用Lambda表达式直接重写一些比较简单的 run() 方法,例如:
new Thread(() -> System.out.println("这是一个线程!")).start();
Lambda表达式的优点
- 避免匿名内部类定义过多;
- 可以让你的代码更为简洁;
- 去掉了一堆没有意义的代码;
Lambda一步步简化的过程:
package com.zeiyalo.lambda;
/**
* @author Zeiyalo
*/
public class MyLambda {
/**
* 2、静态内部类
* 通过静态内部类实现函数式接口
*/
static class Like2 implements ILike {
@Override
public void lambda() {
System.out.println("I like lambda 2");
}
}
public static void main(String[] args) {
/**
* 1、实现类
*/
ILike like1 = new Like1();
like1.lambda();
/**
* 2、静态内部类
*/
like1 = new Like2();
like1.lambda();
/**
* 3、局部内部类
*/
class Like3 implements ILike {
@Override
public void lambda() {
System.out.println("I like lambda 3");
}
}
like1 = new Like3();
like1.lambda();
/**
* 4、匿名内部类
* 可以直接省略内部类的名称,借助接口或者父类;
*/
like1 = new ILike() {
@Override
public void lambda() {
System.out.println("I like lambda 4");
}
};
like1.lambda();
/**
* 5、lambda表达式
* 将匿名内部类其他的无用信息也直接省略,进一步精简;
*/
like1 = () -> {
System.out.println("I like lambda 5");
};
like1.lambda();
}
}
/**
* 定义一个函数式接口(只有一个方法的接口叫函数式接口)
*/
interface ILike {
void lambda();
}
/**
* 1、实现类
* 通过实现类来调用函数式接口;
*/
class Like1 implements ILike {
@Override
public void lambda() {
System.out.println("I like lambda 1");
}
}
简化lambda表达式:
package com.zeiyalo.lambda;
/**
* @author Zeiyalo
*/
public class MyLambda1 {
public static void main(String[] args) {
ILove love;
/**
* 使用lambda表达式,前提时接口为函数式接口
*/
love = (String a) -> {
System.out.println("I love you," + a);
};
love.love("芳芳");
/**
* 参数类型可以去掉
* 如果是多个参数,去掉时应该一起去掉参数类型;
*/
love = a -> {
System.out.println("I love you," + a);
};
love.love("芳芳");
/**
* 如果只有一行代码,可以去掉花括号,否则应该写在代码块中
*/
love = a -> System.out.println("I love you," + a);
love.love("芳芳");
}
}
interface ILove {
void love(String a);
}
class Love implements ILove{
@Override
public void love(String a) {
System.out.println("I love you," + a);
}
}
小结:
- 首先使用lambda表达式的前提是为函数式接口;
- 然后可以去掉入参的参数类型,一个入参可以去括号,如果是多个参数,去掉时应该一起去掉参数类型;
- 最后如果只有一行代码,可以去掉花括号,否则应该写在代码块中;
5、线程状态
线程一般有五种状态;
源码里总共有六种:
- **NEW:**线程刚刚创建,还未启动时的状态。
- RUNNABLE:线程在JAVA虚拟机中执行的状态。
- BLOCKED:线程被阻塞等待监视器锁定的状态。
- WAITING: 线程无限期等待另一个线程执行特定操作的状态(需手动唤醒)。
- TIMED_WAITING: 线程正在等待另一个线程执行最多等待时间的操作的状态(会自动唤醒)。
- **TERMINATED:**线程已退出的状态。
线程停止
- 建议线程正常停止 ——> 利用次数循环,不建议使用死循环;
- 建议使用标志位 ——> 设置一个标志位;
- 不建议使用stop或者destroy等JDK中已过时的方法;
package com.zeiyalo.state;
/**
* @author Zeiyalo
*/
public class TestStop implements Runnable{
private boolean flag = true;
@Override
public void run() {
int i = 0;
if (flag) {
System.out.println("该线程正在运行---------" + i++);
}
}
public void stop() {
flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main 运行中" + i);
if(i == 900) {
testStop.stop();
System.out.println("线程停止了---------------");
}
}
}
}
线程休眠
通过sleep方法可以让线程进入休眠,到了规定时间在自动唤醒,线程进入 TIMED_WAITING 的状态;
1、通过线程休眠来做网络延时;
package com.zeiyalo.state;
import com.zeiyalo.demo01.TicketRunnable;
/**
* @author Zeiyalo
*/
public class TestSleep implements Runnable {
// 票数
private int num = 10;
@Override
public void run() {
while (true) {
if (num <= 0) {
break;
}
// 模拟延时
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + num + "张票!");
num--;
}
}
public static void main(String[] args) {
// 卖票线程
TicketRunnable runnable = new TicketRunnable();
// 多个抢票线程
new Thread(runnable, "小明").start();
new Thread(runnable, "大红").start();
new Thread(runnable, "梁江").start();
new Thread(runnable, "余华").start();
}
}
2、通过线程休眠来做倒计时;
package com.zeiyalo.state;
/**
* @author Zeiyalo
*/
public class TestSleep2 implements Runnable{
/**
* 倒计时
*/
@Override
public void run() {
int num = 10;
while (true) {
System.out.println(num--);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (num<=0) {
break;
}
}
}
public static void main(String[] args) {
TestSleep2 sleep2 = new TestSleep2();
new Thread(sleep2).start();
}
}
3、通过线程休眠来获取当前系统时间;
package com.zeiyalo.state;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author Zeiyalo
*/
public class TestSleep1 {
/**
* 打印系统当前时间
*/
public static void main(String[] args) throws InterruptedException {
int num = 10;
Date date = new Date(System.currentTimeMillis());
while (true) {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
Thread.sleep(1000);
date = new Date(System.currentTimeMillis());
if (num-- <= 0) {
break;
}
}
}
}
线程礼让——yield
- 礼让线程,让当前的线程暂停,但不阻塞;
- 将线程从运行状态转换为就绪状态;
- 线程礼让不一定成功,主要看CPU的调度结果;
package com.zeiyalo.state;
/**
* @author Zeiyalo
*/
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() + "线程执行完毕!");
}
}
线程强制执行——join
- 使用Join后,其他线程阻塞,有限跑join线程;
- 相当于插队;
package com.zeiyalo.state;
/**
* @author Zeiyalo
*/
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
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 < 100; i++) {
if (i == 20) {
thread.join();
}
System.out.println("main线程 -----" + i);
}
}
}
线程状态观测
线程状态就是上面的所说的六个状态;
注意
- 线程一旦死亡后,便无法再启动,对已死亡的线程调用start方法将会报错;
6、线程的优先级
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器根据线程的优先级来决定应该调度哪个线程来执行;
- 线程优先级用数字表示,范围从1~10;
- 使用以下方式改变或获取优先级(getPriority(), setPriority(int —));
package com.zeiyalo.state;
/**
* @author Zeiyalo
*/
public class TestPriority implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "的优先级为 ---->" + Thread.currentThread().getPriority());
}
public static void main(String[] args) {
TestPriority testPriority = new TestPriority();
Thread t1 = new Thread(testPriority);
Thread t2 = new Thread(testPriority);
Thread t3 = new Thread(testPriority);
/**
* Thread-2的优先级为 ---->5
* Thread-1的优先级为 ---->5
* Thread-0的优先级为 ---->5
*
* 推测默认的优先级为5
*/
t1.start();
t2.start();
t3.start();
Thread t4 = new Thread(testPriority);
Thread t5 = new Thread(testPriority);
t4.setPriority(9);
t5.setPriority(3);
t5.start();
t4.start();
/**
* 多次测试结果显示并非优先级高的一定优先执行,只是代表他被优先调度的概率大一些;
*/
}
}
注意
- 并非优先级高的一定优先执行,只是代表他被优先调度的概率大一些;
7、线程同步
不安全得三个线程问题:
不安全的买票
package com.zeiyalo.syn;
/**
* @author Zeiyalo
*/
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "老师").start();
new Thread(buyTicket, "我").start();
new Thread(buyTicket, "黄牛").start();
}
}
class BuyTicket implements Runnable {
private int ticketNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
buy();
}
}
private void buy() {
if (ticketNum <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- +"张票");
}
}
暂时并未出现不安全问题,但其实是不安全的;(黄牛抢票确实厉害)
银行取钱问题
package com.zeiyalo.syn;
/**
* @author Zeiyalo
*/
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "家庭基金");
new Drawing(50, account, "你").start();
new Drawing(100, account, "老妈").start();
}
}
class Account {
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class Drawing extends Thread {
private Account account;
private int drawingMoney;
private int nowMoney;
public Drawing(int drawingMoney, Account account, String name) {
super(name);
this.drawingMoney = drawingMoney;
this.account = account;
}
@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 = nowMoney + drawingMoney;
System.out.println(account.name + "卡内余额为" + account.money);
System.out.println(Thread.currentThread().getName() + "手里的钱" + nowMoney);
}
}
测试结果:
集合的不安全性
package com.zeiyalo.syn;
import java.util.ArrayList;
/**
* @author Zeiyalo
*/
public class UnsafeList {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() ->
list.add(Thread.currentThread().getName())
).start();
}
System.out.println(list.size());
}
}
测试结果:
测试结果到不了一万,因为多个线程添加同时覆盖了一些相同list的位置,所以导致了到不了一万,说明其实这个List中的add方法其实不是线程安全的;
解决上述问题,我们可以使用锁机制来解决上述问题;
synchronized关键字
使用 关键字可以实现对特定资源的加锁,首先介绍同步方法;
同步方法:
- 同步方法可以用来锁定一个方法,当前线程运行该方法时,将会独占该锁,其他需要使用该方法的线程将堵塞,直到方法返回才会释放该锁,后面被阻塞的方法才能获得该锁,继续执行;
- 弊端是一个大的复杂的方法时使用同步方法就会导致效率变的很慢,当一个方法中不仅有修改资源的操作,还有读取资源的操作时,读取资源的操作时间也被锁进去,很浪费资源;
上面同步方法的弊端显而易见,所以不是适用很多情况,这个时候就需要用到同步代码块;
同步代码块:
- 同步代码块可以锁定对象或者属性,将对应的公共资源锁住后,在执行时就会查询该共享资源是否被锁住,如果被锁住,需要使用该共享资源的线程将被阻塞,否则将持锁运行;
- 较为便利通过 synchronized (Object)可以较为灵活的去控制只在对应代码块锁住对应共享资源,不至于造成资源浪费;
买票案例(同步方法)
package com.zeiyalo.syn;
/**
* @author Zeiyalo
*/
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "老师").start();
new Thread(buyTicket, "我").start();
new Thread(buyTicket, "黄牛").start();
}
}
class BuyTicket implements Runnable {
private int ticketNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
buy();
}
}
private synchronized void buy() {
if (ticketNum <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- +"张票");
}
}
测试结果:
老师将所有的票买走,说明代码还有问题,当代码进入run方法时,持锁运行,因为flag未被置零,所以run中间的buy方法会反复进行,所以建议可以在run方法中加入sleep方法,这样在buy方法运行前或者运行后,当时当前线程还未持有锁或者已经释放方法锁资源,其他线程可以进入buy方法持锁运行;
注意:
- sleep不能放在buy方法中,因为sleep不会释放锁资源,在方法中睡眠,锁资源还是不会释放,其他线程无法获取锁资源,也会被卡住,然后还是只能看着老师独自抢掉所有票;
- sleep放在run方法中调用buy方法之前和之后的区别:
- 之前:在之前调用sleep,第一个线程来了,直接先睡眠,然后其他线程进入,再次睡眠,第一个睡醒的线程拿到锁资源,运行之后,释放,CPU调度下一个循环睡眠起来的线程继续获取锁资源(这个是随机的,看CPU调度);
- 之后:在之后调用sleep,第一个线程运行buy方法,其他线程被阻塞在buy方法之前请求CPU调度,然后第一个线程运行完,睡眠,因为第一个线程已经运行完一次buy方法,所以已经释放锁资源,这时按CPU调度给一个线程分配锁资源,然后循环运行;
- 两者其实没啥区别,硬要说区别就是第一次进来的线程在第一种不一定第一个执行buy方法,第二个应该是第一个线程先执行buy方法;
修改后的代码
package com.zeiyalo.syn;
/**
* @author Zeiyalo
*/
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "老师").start();
new Thread(buyTicket, "我").start();
new Thread(buyTicket, "黄牛").start();
}
}
class BuyTicket implements Runnable {
private int ticketNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
buy();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void buy() {
if (ticketNum <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- +"张票");
}
}
测试结果:
由于测试代码为sleep在buy之后,根据之前分析,应该都为第一个线程第一个获取锁资源,测试多次,基本上都是老师第一个获取第十张票;
测试CopyOnWriteArrayList
CopyOnWriteArrayList 是 JUC 包中的一个线程安全的链表;
CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略。同时因为获取—修改—写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对list数组进行修改。
实现方法:
- 每个CopyOnWriteArrayList对象里面有一个array数组对象用来存放具体元素;
- ReentrantLock独占锁对象用来保证同时只有一个线程对array进行修改;
测试代码:
package com.zeiyalo.syn;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author Zeiyalo
*/
public class TestJUC {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> name = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> name.add(Thread.currentThread().getName())).start();
}
Thread.sleep(1000);
System.out.println(name.size());
}
/**
* 10000
*
* Process finished with exit code 0
*/
}
8、死锁
8.1、什么是死锁?
死锁,就是指两个或者两个以上的线程,在抢占两个或更多资源时,互相占有了对方所需要的资源,然后导致互相无限等待的情况;
8.2、死锁形成的条件?(四大条件)
死锁的形成有以下四大条件:
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程会因为请求资源不到而被阻塞,对已获得的资源保持不放;
- 不可强行占有条件:在一个资源被进程获取到之前,未使用完之前,不可被强行剥夺;
- 循环等待条件:多个进程之间形成一种头尾相接的循环等待资源的关系;
8.3、死锁问题怎么避免?
对于死锁问题,可以通过破坏四个条件中的一个或者多个来进行解决;
- 互斥条件:无法被破坏;
- 请求与保持条件:可以一次性请求所有资源,并且在请求过程中先阻塞该进程,直到所有请求已就位,但该方法效率很低,不建议使用;
- 不可抢占条件:
- 可以限制当目前进程获取一个资源失败的时候,应释放之前按所持有的资源;
- 或者如果当一个进程获取资源发现被另外一个进程占有时,可以请求操作系统抢占另一个进程,要求其释放已占有资源;
- 循环等待条件:可以限定资源的获取顺序,每个进程都应该按照一定顺序去获取资源;
8.4、死锁的案例
化妆品抢占问题;
化妆需要口红和镜子,当两个人同时需要口红和镜子时,各自拿了一个然后等待另外一个释放;
package com.zeiyalo.syn;
/**
* @author Zeiyalo
*/
public class DeadLock {
public static void main(String[] args) {
MakeUp mother = new MakeUp(0, "mother");
MakeUp wife = new MakeUp(1, "wife");
wife.start();
mother.start();
}
}
/**
* 口红
*/
class LipStick {
}
/**
* 镜子
*/
class Mirror {
}
class MakeUp extends Thread {
static Mirror mirror = new Mirror();
static LipStick lipStick = new LipStick();
private int choice;
public MakeUp(int choice, String name){
super(name);
this.choice = choice;
}
@Override
public void run() {
try {
makeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void makeUp() throws InterruptedException {
if (choice == 0) {
synchronized (mirror) {
System.out.println(Thread.currentThread().getName() + "获得了镜子!");
sleep(1000);
synchronized (lipStick) {
System.out.println(Thread.currentThread().getName() + "获得了口红!");
}
}
} else {
synchronized (lipStick) {
System.out.println(Thread.currentThread().getName() + "获得了口红!");
sleep(1000);
synchronized (mirror) {
System.out.println(Thread.currentThread().getName() + "获得了镜子!");
}
}
}
}
}
上面代码就会产生死锁问题,下面是一种修改方法:
package com.zeiyalo.syn;
/**
* @author Zeiyalo
*/
public class DeadLock {
public static void main(String[] args) {
MakeUp mother = new MakeUp(0, "mother");
MakeUp wife = new MakeUp(1, "wife");
wife.start();
mother.start();
}
}
/**
* 口红
*/
class LipStick {
}
/**
* 镜子
*/
class Mirror {
}
class MakeUp extends Thread {
static Mirror mirror = new Mirror();
static LipStick lipStick = new LipStick();
private int choice;
public MakeUp(int choice, String name){
super(name);
this.choice = choice;
}
@Override
public void run() {
try {
makeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void makeUp() throws InterruptedException {
if (choice == 0) {
synchronized (mirror) {
System.out.println(Thread.currentThread().getName() + "获得了镜子!");
sleep(1000);
}
synchronized (lipStick) {
System.out.println(Thread.currentThread().getName() + "获得了口红!");
}
} else {
synchronized (lipStick) {
System.out.println(Thread.currentThread().getName() + "获得了口红!");
sleep(1000);
}
synchronized (mirror) {
System.out.println(Thread.currentThread().getName() + "获得了镜子!");
}
}
}
/**
* mother获得了镜子!
* wife获得了口红!
* mother获得了口红!
* wife获得了镜子!
*
* Process finished with exit code 0
*/
}
还有其他的修改方式,大伙可以自己试试,或者以后有时间可以更上来,先不做过多赘述;
9、ReentrantLock(可重入锁)
在Java中定义了可重入锁,可以更方便的让我们锁住自己要锁住的部分代码;
package com.zeiyalo.syn;
/**
* @author Zeiyalo
*/
public class TestLock {
public static void main(String[] args) {
BuyTicket2 buyTicket = new BuyTicket2();
new Thread(buyTicket, "老师").start();
new Thread(buyTicket, "我").start();
new Thread(buyTicket, "黄牛").start();
}
}
class BuyTicket2 implements Runnable {
private int ticketNum = 10;
@Override
public void run() {
while (true) {
if (ticketNum > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNum--);
} else {
break;
}
}
}
/**
* 9
* 8
* 10
* 7
* 6
* 5
* 4
* 3
* 4
* 2
* 0
* 1
*
* Process finished with exit code 0
*/
}
明显可以看出出现了线程安全问题;
使用可重入锁:
package com.zeiyalo.syn;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Zeiyalo
*/
public class TestLock {
public static void main(String[] args) {
BuyTicket2 buyTicket = new BuyTicket2();
new Thread(buyTicket, "老师").start();
new Thread(buyTicket, "我").start();
new Thread(buyTicket, "黄牛").start();
}
}
class BuyTicket2 implements Runnable {
private int ticketNum = 10;
/**
* 初始化锁
*/
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 加锁
lock.lock();
try {
// 执行代码块
if (ticketNum > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNum--);
} else {
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
}
}
/**
* 10
* 9
* 8
* 7
* 6
* 5
* 4
* 3
* 2
* 1
*
* Process finished with exit code 0
*/
下面就是可重入锁的常用格式:
// 加锁
lock.lock();
try {
// 执行代码块
} else {
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
可重入锁和synchronized的区别:
- ReentrantLock 是 JUC 包下的一个对象,而 synchronized 是Java的关键字;
- 底层实现不同:
- ReentrantLock 是通过CAS保证线程操作的原子性,和volatile关键字保证它的数据可见性来实现锁功能;
- synchronized 的实现则是
- ReentrantLock 可以对代码块进行加锁操作,而synchronized 不仅可以对代码块加锁还可以对方法加锁;
- Lock锁使用时,JVM将花费较少的时间来调度线程,性能更好,而且它有更好的扩展性(提供更多子类)(jdk1.6之前,后来退出来了synchronized锁升级制度,两者性能就差不);