文章目录
Java多线程并发
进程与线程的联系和区别
进程
正在运行的程序,是系统资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源
线程
是进程的单个顺序控制流,或者说是一个单独执行的路径。一个进程如果只有一条执行路径,称之为单线程程序。一个进程如果有多条执行路径,称之为多线程程序。线程是包含在进程中的。
并行与并发
并行
并行是指在两个或多个事件在同一时刻发生。专业点来说就是:在同一时刻,有多条指定在多个处理器上同时执行。所以无论从宏观上还是微观上来看,二者都是一起执行的。
并发
并发是指两个多个事件在同一时间间隔发生。专业点来说就是:在同一时刻只能有一条指令执行,但是多个进行指令被快速轮换执行,使得在宏观上具有多个线程同时执行的效果,但在微观上并不是同时执行的。只是把时间分成若干段,使得多个进行快速交替执行。
Java此程序的运行原理
Java命令会启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个主线程,然后主线程去调用某个类的main方法。多以main方法是运行在主线程中的。也就是说呢,在之前的Java学习中写的程序都是单线程的。但是JVM本身启动时就是多线程,因为JVM至少有主线程和垃圾回收线程。
线程的调度
假设计算机只有一个CPU,CPU在某一个时刻只能执行一条指令,线程只有得到了CPU时间片,也就是使用权,才可以执行指令。那么具体应该是哪个线程得到CPU呢,这就回到了线程调度的问题。
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程所占用的CPU的时间。
- 抢占式调度模型:优先级高的线程优先使用CPU。如果优先级相同,那么随机选择一个,优先级高的获得的CPU时间片相对多一些。java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
多线程的实现方式
继承Thread类,并重写run()方法
package csu.edu.cn;
/**
* 多线程实现方式一:继承Thread并重写run()方法
*/
public class Thread01 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
package csu.edu.cn;
/**
* 测试类
*/
public class Test01 {
public static void main(String[] args) {
//创建两个线程
Thread01 thread01 = new Thread01();
Thread01 thread02 = new Thread01();
/**
*两个线程都调用start方法
* 然后由JVM去调用run()方法
*/
thread01.start();
thread02.start();
}
}
结果:0-9打印两遍
特别注意:
- 不能直接用线程对象去调用run()方法,直接调用run()方法,就相当于调用普通方法,与多线程无关。要想看到多线程的效果就必须调用start()方法来启动。
- 调用run()与调用start()的区别:
- run()的调用仅仅是直接封装了被线程执行的代码,但是直接调用的话是普通方法的调用。
- start()方法的调用,首先单独启动了一个线程,然后再由JVM去该线程的run()方法
- 线程调用的是start()方法,而实际上却调用的是run()方法定义的主体。
- 同一个线程不能被start()两次。
获取和设置线程的名字
package csu.edu.cn;
/**
* 给线程设置名字
*/
public class Thread02 extends Thread{
public Thread02() {
super();
}
public Thread02(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName()+"-----"+i);
}
}
}
package csu.edu.cn;
/**
* 测试类
*/
public class Test02 {
public static void main(String[] args) {
//通过构造方法取名字
Thread02 t1 = new Thread02("黄");
Thread02 t2 = new Thread02("李");
t1.start();
t2.start();
Thread02 t3 = new Thread02("脏");
Thread02 t4 = new Thread02("刘");
Thread02 t5 = new Thread02("怀");
t3.start();
t4.start();
t5.start();
//public static Thread currentThread()返回对当前正在执行的线程对象的引用
//通过currentThread()获取当前的线程对象,再通过getName()获取线程名字
//拿这个程序的main方法的主线程举例
System.out.println(Thread.currentThread().getName());
}
}
/**
李-----0
怀-----0
怀-----1
刘-----0
刘-----1
刘-----2
黄-----0
脏-----0
main
脏-----1
脏-----2
脏-----3
脏-----4
黄-----1
刘-----3
怀-----2
怀-----3
怀-----4
李-----1
李-----2
李-----3
李-----4
刘-----4
黄-----2
黄-----3
黄-----4
**/
结果分析:从此处的打印结果来看,线程之间是抢夺的方式来占据CPU的,这也我们上面说的Java虚拟机采用抢占式调度模型是一致的。只有占据了CPU的线程才能运行,第一次打印李-----0表示被t2抢夺到了CPU的执行权,但是只执行了一条语句CPU的时间就被夺走了。各个线程交错运行,每个线程只占据一小段时间,这样看起来像是一起运行的实际上不是。t1时间结束后就被t5抢占了CPU,打印了怀—0.
另外这里面还夹着一个字符串 “main”,这是System.out.println(Thread.currentThread().getName());语句的输出结果,因为main方法是被主线程调用的,也需要抢占CPU,所以打印时间也是不确定的。
实现Runable接口
package csu.edu.cn;
/**
* 实现Runbale接口
*/
public class Thread03 implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
package csu.edu.cn;
/**
*测试类
*/
public class Test03 {
public static void main(String[] args) {
Thread03 thread03 = new Thread03();
Thread t1 = new Thread(thread03);
Thread t2 = new Thread(thread03);
//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用target.run()
t1.setName("A");
t2.setName("B");
t1.start();
t2.start();
// System.out.println(Thread.currentThread().getName());
}
}
结果:
B---0
A---0
B---1
A---1
A---2
B---2
A---3
B---3
B---4
B---5
B---6
A---4
B---7
B---8
A---5
B---9
A---6
A---7
A---8
A---9
- 通过Runnable接口实现的类可以继承其他类,继承Thread的类不行。可以避免由于Java单继承带来的局限性。
- Runnable适合多个相同程序的代码去处理同一个资源的情况,比如
package csu.edu.cn; /** * 使用Runable处理同一资源 */ public class Thread04 implements Runnable{ int num = 20; @Override public void run() { while(true){ if(num>0){ System.out.println(Thread.currentThread().getName()+"----"+(num--)); } } } } package csu.edu.cn; /** * 测试类 */ public class Test04 { public static void main(String[] args) { Thread04 thread04 = new Thread04(); Thread t1 = new Thread(thread04,"窗口一"); Thread t2 = new Thread(thread04,"窗口二"); Thread t3 = new Thread(thread04,"窗口三"); t1.start(); t2.start(); t3.start(); } } 结果: 窗口一----20 窗口三----18 窗口二----19 窗口三----16 窗口一----17 窗口三----14 窗口二----15 窗口三----12 窗口一----13 窗口三----10 窗口二----11 窗口三----8 窗口三----6 窗口三----5 窗口一----9 窗口一----3 窗口一----2 窗口一----1 窗口三----4 窗口二----7
可以看到这里是共享了同一个num
多线程-龟兔赛跑经典案例
package csu.edu.cn;
/**
* 多线程----龟兔赛跑案例
*/
public class Race implements Runnable{
private String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
//如果是兔子,就休息
if(Thread.currentThread().getName().equals("兔子") && i%10==0){
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 is "+winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"乌龟").start();
new Thread(race,"兔子").start();
}
}
实现Callable接口(了解即可)
核心方法:call()有返回值
package csu.edu.cn;
import java.util.concurrent.*;
public class Test05 implements Callable<Boolean>{
String name;
String age;
public Test05(String name,String age) {
this.age = age;
this.name = name;
}
@Override
public Boolean call() throws Exception {
System.out.println(name+"---"+age);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步:创建服务
Test05 t1 = new Test05("小鲤鱼","12");
Test05 t2 = new Test05("灭聚石塔","23");
Test05 t3 = new Test05("鳄鱼","18");
//第二步:创建执行服务
ExecutorService service = Executors.newFixedThreadPool(3);
//第三步:提交执行
Future<Boolean> r1 = service.submit(t1);
Future<Boolean> r2 = service.submit(t2);
Future<Boolean> r3 = service.submit(t3);
//第四步:获取结果
boolean rs1 = r1.get();
boolean rs2 = r1.get();
boolean rs3 = r1.get();
//第五步:关闭服务
service.shutdown();
}
}
线程相关状态
-
新建状态(NEW)
- 即使用new新建一个线程,这个线程就处于新建状态
-
运行状态(RUNABLE)
- 操作系统中的就绪和运行两种状态,在Java中统称为RUNNABLE。
- 就绪状态(READY):当线程对象调用了start()方法之后,线程就处于就绪状态,就绪意味着可以执行,但具体什么时候执行取决于JVM里线程调度情况。
- 运行状态(RUNNING):当处于就绪状态的线程获得了CPU之后,真正开始执行run()方法的线程执行体时,就意味着该线程处于运行状态,但是对于但单处理器,一个时刻只能由一个线程处于运行状态。运行状态转变为就绪状态的情形:
- 线程失去处理器资源。线程不一定完整执行的,执行到一半,说不定就被别的线程抢走了。
- 调用yield()静态方法,暂时暂停当前线程,让系统的线程调度器重新调度一次,它自己完全有可能再次运行。
- 操作系统中的就绪和运行两种状态,在Java中统称为RUNNABLE。
-
阻塞状态(BLOCKED)
-
阻塞状态表示线程正等待监视器锁,而陷入的状态。
以下场景线程将会阻塞:
- 线程等待进入synchronized同步方法。
- 线程等待进入synchronized同步代码块。
线程取得锁,就会从阻塞状态转变为就绪状态。
-
-
等待状态(WAITING)
-
超时等待状态(TIMED_WAITING)
-
消亡状态
-
即线程的终止,表示线程已经执行完毕。前面已经说了,已经消亡的线程不能通过start再次唤醒。
- run()和call()线程执行体中顺利执行完毕,线程正常终止。
- 线程抛出一个没有捕获的Exception或Error。
需要注意的是:主线成和子线程互不影响,子线程并不会因为主线程结束就结束。
-
线程休眠sleep()方法
Thread.sleep()是Thread类的一个静态方法,使当前线程休眠,进入阻塞状态(暂停执行),如果线程在睡眠状态被中断,将会抛出IterruptedException中断异常。
线程休眠常用于做倒计时等作用
package csu.edu.cn;
/**
* 线程休眠案例:倒计时
*/
public class TestSleep implements Runnable{
@Override
public void run() {
for (int i = 10; i >= 0; i--) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("距离结束还剩:"+i+"秒");
}
System.out.println("时间结束!");
}
public static void main(String[] args) {
TestSleep testSleep = new TestSleep();
Thread thread = new Thread(testSleep);
thread.start();
}
}
//打印结果:
距离结束还剩:10秒
距离结束还剩:9秒
距离结束还剩:8秒
距离结束还剩:7秒
距离结束还剩:6秒
距离结束还剩:5秒
距离结束还剩:4秒
距离结束还剩:3秒
距离结束还剩:2秒
距离结束还剩:1秒
距离结束还剩:0秒
时间结束!
package csu.edu.cn;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.SimpleFormatter;
/**
* 线程休眠案例:钟表
*/
public class TestSleep implements Runnable{
@Override
public void run() {
Date date = new Date(System.currentTimeMillis());
while(true){
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
date = new Date(System.currentTimeMillis());
}
}
public static void main(String[] args) {
TestSleep testSleep = new TestSleep();
Thread thread = new Thread(testSleep);
thread.start();
}
}
打印结果:
16:50:55
16:50:56
16:50:57
16:50:58
16:50:59
16:51:00
16:51:01
16:51:02
16:51:03
16:51:04
16:51:05
16:51:06
16:51:07
16:51:08
16:51:09
···
线程礼让yield()
package csu.edu.cn;
/**
* 线程礼让
*/
public class TestYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程结束");
}
public static void main(String[] args) {
TestYield testYield = new TestYield();
Thread thread1 = new Thread(testYield);
Thread thread2 = new Thread(testYield);
thread1.start();
thread2.start();
}
}
打印结果:
Thread-1线程开始
Thread-0线程开始
Thread-0线程结束
Thread-1线程结束
类似于这样一人一句,进程占用CPU打印完后会礼让一下资源,当然礼让过后依然抢到了也是有可能的
线程强制执行join
package csu.edu.cn;
/**
* 线程强制执行
*/
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("老子是VIP线程!!"+i);
}
}
public static void main(String[] args) {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//执行主线程
for (int i = 0; i < 8; i++) {
System.out.println("主线程---"+i);
if(i==3){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
结果:
主线程---0
老子是VIP线程!!0
主线程---1
主线程---2
主线程---3
老子是VIP线程!!1
老子是VIP线程!!2
老子是VIP线程!!3
老子是VIP线程!!4
老子是VIP线程!!5
老子是VIP线程!!6
老子是VIP线程!!7
老子是VIP线程!!8
老子是VIP线程!!9
主线程---4
主线程---5
主线程---6
主线程---7
线程优先级
package csu.edu.cn;
public class TestPriority implements Runnable{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"我正在运行"+"我的优先级是:"+Thread.currentThread().getPriority());
}
}
public static void main(String[] args) {
TestPriority testPriority = new TestPriority();
Thread thread1 = new Thread(testPriority,"1");
Thread thread2 = new Thread(testPriority,"2");
Thread thread3 = new Thread(testPriority,"3");
Thread thread4 = new Thread(testPriority,"4");
Thread thread5 = new Thread(testPriority,"5");
System.out.println(Thread.currentThread().getPriority());
thread1.setPriority(2);
thread2.setPriority(2);
thread3.setPriority(3);
thread4.setPriority(10);
thread5.setPriority(3);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
//结果:
5
1我正在运行我的优先级是:2
2我正在运行我的优先级是:2
3我正在运行我的优先级是:3
4我正在运行我的优先级是:10
4我正在运行我的优先级是:10
4我正在运行我的优先级是:10
3我正在运行我的优先级是:3
2我正在运行我的优先级是:2
5我正在运行我的优先级是:3
5我正在运行我的优先级是:3
1我正在运行我的优先级是:2
5我正在运行我的优先级是:3
2我正在运行我的优先级是:2
3我正在运行我的优先级是:3
1我正在运行我的优先级是:2
从上面的运行结果来看,默认的线程的优先级是5,最大是10,最小是1
线程的优先级最高并不意味着线程会优先执行,只是优先执行的概率会更高。
- 线程的默认优先级为5
- 线程优先级的范围是1-10
- 线程优先级高仅仅表示的是获取CPU时间片的几率会高一些,但不代表优先级高的线程一定能抢到CPU
守护线程
线程分为守护线程和用户线程,普通线程为用户线程,包括调用main方法的主线程也是用户线程,而垃圾回线程属于守护线程。
JVM必须等待用户线程执行完毕,但是不需要等待守护线程执行完毕 。守护线程一般是在用户线程结束后一段时间结束。下面用一个例子来说明这一点。
package csu.edu.cn;
/**
* 守护线程实例
*/
public class TestDaemon implements Runnable{
@Override
public void run() {
while(true){
System.out.println("上帝一直在看着你的努力奋斗!!");
}
}
public static void main(String[] args) {
TestDaemon testDaemon = new TestDaemon();
Thread thread = new Thread(testDaemon,"黄佳浩");
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 12; i++) {
System.out.println("我是普通平民,正在寒窗苦读。。。。。");
}
System.out.println("我终于考上了中南大学!!");
}
}
//运行结果:
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我是普通平民,正在寒窗苦读。。。。。
我终于考上了中南大学!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
上帝一直在看着你的努力奋斗!!
可以看到上面的与运行结果,表明上帝这个守护线程在用户线程执行结束后一段时间才停止运行。
线程同步机制
买火车票问题
package csu.edu.cn;
/**
* 不安全的买票机制
*/
public class TestTicket {
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 ticket = 10;
@Override
public void run(){
while(true){
if(ticket>0){
try {
Thread.sleep(100); //语句一
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在购买第 "+ticket+" 张票"); //语句二
ticket--; //语句三
}
}
}
}
//打印结果:
王睿正在购买第 10 张票
张郑旭正在购买第 10 张票
史惠文正在购买第 10 张票
王睿正在购买第 7 张票
史惠文正在购买第 7 张票
张郑旭正在购买第 7 张票
史惠文正在购买第 4 张票
张郑旭正在购买第 4 张票
王睿正在购买第 4 张票
张郑旭正在购买第 1 张票
史惠文正在购买第 1 张票
王睿正在购买第 1 张票
结果分析:
很明显我们看到了的现象是不同人正在购买同一张票,这就是线程不安全的表现之一。出现这个现象的重要原因就是CPU的操作具有原子性,单独执行一条指令后或者说语句,在执行完毕之前不会被中断。三个线程启动后,都会处于就绪状态,然后开始抢夺CPU执行语句。
- 语句①:Thread.sleep(1000);
- 语句②: System.out.println(Thread.currentThread().getName()+“正在购买第 “+ticket+” 张票”);
- 语句③: ticket–;
三条线程,假设线程1抢到了CPU,这时候就会开始执行语句,也就是至少会完成语句1,然后开始休眠。但是如果线程1不是休眠语句,那么线程1就可以继续往下执行,因为原子性,正在执行的语句不会被打断,所有只会在一条语句结束,下一条语句开始前,被抢走CPU或者中断,导致线程退出运行状态,转为就绪会阻塞状态。所以线程1可能一次性执行完了所有语句,也可能刚刚执行完就被抢走了CPU
接着线程2、3也抢到了CPU,也开执行语句1,然后也进入休眠状态,之后线程123从休眠中醒来,开始争夺CPU来完成语句2,但是都在执行语句3之前被抢走CPU执行语句2去了,所以一直没有进行减一操作。所以可能出现打印的都是正在去第十张票的现象。然后又开始抢夺CPU,接连执行三次-1的操作,然后又开始抢夺CPU执行语句1,所以出现以上打印的结果。
这是非常常见的线程不安全问题。
发生线程不安全的三个条件
- 存在多个线程
- 存在共享变量或i数据
- 多条语句操作共享数据或变量
解决思路
可以看到火车票问题,条件1和条件2无法破坏,我们只能着手去破坏3。
思路是将多条语句包装程一个同步代码块,当某个线程执行到这个同步代码快的时候,就类似于原子性一样,其他线程无法抢占CPU,只能等待这个代码块执行完毕。
方法:
1.synchronized——自动锁
2.lock——手动锁
synchronized
synchronized(obj){
//可能会发生线程安全问题的代码
}//这里的obj可以是任意对象,Object obj = new Object()
使用条件
- 必须有两个或两个以上的线程
- 同一时刻只有一个线程能够执行同步代码块
- 多个线程想要同步时,必须使用同一把锁。
使用过程
- 只有抢到锁的线程才能执行同步代码块,其余的线程即使抢到了CPU执行权,也只能被迫等待锁的释放。
- 代码执行完毕或者程序抛出异常都会释放锁,然后还未执行同步代码块的线程争夺锁,谁抢到谁执行同步代码块。
synchronized同步代码块改进买火车票问题
package csu.edu.cn;
/**
* 不安全的买票机制
*/
public class TestTicket {
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 ticket = 10;
Object object = new Object();
@Override
public void run(){
while(true){
synchronized (object){
if(ticket>0){
try {
Thread.sleep(5000); //语句一
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在购买第 "+ticket+" 张票"); //语句二
ticket--; //语句三
}
}
}
}
}
//输出结果:
张郑旭正在购买第 10 张票
张郑旭正在购买第 9 张票
张郑旭正在购买第 8 张票
张郑旭正在购买第 7 张票
张郑旭正在购买第 6 张票
王睿正在购买第 5 张票
王睿正在购买第 4 张票
王睿正在购买第 3 张票
王睿正在购买第 2 张票
王睿正在购买第 1 张票
很明显我们可以看到,结果符合预期。
使用synchronized同步方法改进
package csu.edu.cn;
/**
* 不安全的买票机制
*/
public class TestTicket_02 {
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 ticket = 10;
Object object = new Object();
@Override
public void run(){
while(true){
synchronized (object){
if(ticket>0){
try {
Thread.sleep(5000); //语句一
} catch (InterruptedException e) {
e.printStackTrace();
}
buyTicket();
}
}
}
}
//同步方法,在权限修饰符后面加synchronized
public synchronized void buyTicket(){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在购买第 "+ticket+" 张票"); //语句二
ticket--; //语句三
}
}
}
//打印结果
张郑旭正在购买第 10 张票
张郑旭正在购买第 9 张票
张郑旭正在购买第 8 张票
张郑旭正在购买第 7 张票
张郑旭正在购买第 6 张票
张郑旭正在购买第 5 张票
张郑旭正在购买第 4 张票
史惠文正在购买第 3 张票
王睿正在购买第 2 张票
王睿正在购买第 1 张票
使用同步方法的时候,注意是将synchronized加在权限修饰符后面。同时我们要知道这时候锁的对象时this。这种操作将上面的obj改为this的效果是一样的。
总结:一个线程使用同步方法,另一个线程使用this锁,可以实现同步。
静态同步方法(大致了解即可)
静态同步方法的锁对象是:这个静态同步方法所属的类的字节码文件
下面代码挺长的,但其实就修改了上面同步方法的代码的两处地方
public synchronized void buyTicket(){}改为 public synchronized static void sellTicket(){} synchronized (this){}synchronized (BuyTicket.class){}
死锁问题
package csu.edu.cn;
public class DieLockTest extends Thread{
public boolean flag;
public DieLockTest(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized(LockObject.lockA){
System.out.println("lockA");
synchronized(LockObject.lockB){
System.out.println("lockB");
}
}
}else{
synchronized(LockObject.lockB){
System.out.println("lockB");
synchronized(LockObject.lockA){
System.out.println("lockA");
}
}
}
}
}
package csu.edu.cn;
public class LockObject {
//两个锁对象
public static final Object lockA = new Object();
public static final Object lockB = new Object();
}
package csu.edu.cn;
public class TestDieLock {
public static void main(String[] args) {
DieLockTest t1 = new DieLockTest(true);
DieLockTest t2 = new DieLockTest(false);
t1.start();
t2.start();
}
}
//打印结果
lockA
lockB
//或者
lockB
lockA
分析
可以看到,两个线程都在等待对方释放锁对象,然后进行下一步,但是两者都不释放锁。随即造成了死锁。
Lock
使用Lock解决线程不安全问题
package csu.edu.cn;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 不安全的买票机制
*/
public class TestTicket {
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 ticket = 10;
private Lock lock = new ReentrantLock();
Object object = new Object();
@Override
public void run(){
while(true){
try{
lock.lock();
if(ticket>0){
try {
Thread.sleep(5000); //语句一
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在购买第 "+ticket+" 张票"); //语句二
ticket--; //语句三
}
}finally {
lock.unlock();
}
}
}
}
//打印结果
张郑旭正在购买第 10 张票
张郑旭正在购买第 9 张票
张郑旭正在购买第 8 张票
张郑旭正在购买第 7 张票
史惠文正在购买第 6 张票
史惠文正在购买第 5 张票
史惠文正在购买第 4 张票
王睿正在购买第 3 张票
王睿正在购买第 2 张票
王睿正在购买第 1 张票
//成功解决
写文章不易,给个赞吧!