网站建设网站维护网站外包今日新闻热点大事件
线程的状态
在Java中,线程的生命周期可以分为以下几种状态:
-
新建(New):
-
线程对象被创建,但尚未调用
start()
方法。 -
控制方式:通过创建
Thread
对象或其子类对象。
-
-
就绪(Runnable):
-
线程已经调用了
start()
方法,等待CPU调度执行。 -
控制方式:调用
start()
方法。
-
-
运行(Running):
-
线程获得CPU资源,正在执行
run()
方法中的代码。 -
控制方式:由JVM的线程调度器决定。
-
-
阻塞(Blocked):
-
线程因为某些原因(如等待锁、I/O操作等)暂时停止执行。
-
控制方式:通过
synchronized
关键字、wait()
方法、I/O操作等。
-
-
等待(Waiting):
-
线程无限期等待其他线程执行特定操作(如
notify()
或notifyAll()
)。 -
控制方式:调用
wait()
方法。
-
-
超时等待(Timed Waiting):
-
线程在指定的时间内等待其他线程执行特定操作。
-
控制方式:调用
sleep(long millis)
、wait(long timeout)
、join(long millis)
等方法。
-
-
终止(Terminated):
-
线程执行完
run()
方法或因为异常退出。 -
控制方式:
run()
方法执行完毕或调用stop()
方法(不推荐使用)。
-
控制线程状态的方法
1.新建状态:
Thread thread = new Thread(() -> {// 线程执行的代码
});
2.就绪状态:
thread.start();
3.运行状态:
-
由JVM自动调度,开发者无法直接控制。
4.阻塞状态:
- 使用
synchronized
关键字:
synchronized (lockObject) {// 临界区代码
}
- 使用
wait()
方法:
synchronized (lockObject) {lockObject.wait();
}
5.等待状态:
-
使用
wait()
方法:synchronized (lockObject) {lockObject.wait(); }
6.超时等待状态:
- 使用
sleep()
方法:Thread.sleep(1000); // 休眠1秒
- 使用
wait(long timeout)
方法:synchronized (lockObject) {lockObject.wait(1000); // 等待1秒 }
- 使用
join(long millis)
方法:thread.join(1000); // 等待thread线程1秒
7.终止状态:
run()方法执行完毕:
-
不推荐使用
stop()
方法,因为它可能导致资源未释放等问题。
示例代码:
public class ThreadStateExample {public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread thread = new Thread(() -> {synchronized (lock) {try {System.out.println("Thread is going to wait");lock.wait(); // 进入等待状态System.out.println("Thread is notified and running again");} catch (InterruptedException e) {e.printStackTrace();}}});System.out.println("Thread state after creation: " + thread.getState()); // NEWthread.start();System.out.println("Thread state after start: " + thread.getState()); // RUNNABLEThread.sleep(100); // 确保线程进入等待状态System.out.println("Thread state after wait: " + thread.getState()); // WAITINGsynchronized (lock) {lock.notify(); // 唤醒等待的线程}Thread.sleep(100); // 确保线程被唤醒System.out.println("Thread state after notify: " + thread.getState()); // TERMINATED}
}
练习代码
线程停止
(创建一个标志位flag,测试线程停止)
package com.kuang.thread;/*
线程状态
5个状态
1、new 新生 Thread t = new Thread() 线程对象一旦创建就进入到了新生状态
2、就绪 当调用start()方法,线程立即进入到就绪状态,但不意味着立即调度执行
3、运行状态,调度start完成后,进入运行状态,线程才真正执行线程体的代码块
4、阻塞状态,运行状态时,调用sleep,wait或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待CPU调度执行
5、死亡状态,运行状态时,出现线程中断,或者结束,一旦进入到死亡状态,就不能再次启动线程相关的方法
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 在指定的毫秒数内让正在执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程,尽量别用这个方式,不建议使用
boolean isAlive 测试线程是否处于活动状态停止线程
不推荐使用JDK提供的stop()、destroy()方法,已废弃
推荐线程自己停止下来
建议使用一个标志位进行终止变量,当flag = false,则终止线程运行。测试stop停止线程
1.建议线程正常停止--》利用次数,不建议死循环;
2.建议使用标志位--》设置一个标志位
3.不要使用stop或者destroy等过时或者jdk不建议使用的方法idea中,alt + enter 快捷重写方法*/public class TestStop implements Runnable{//1.设置一个标志位private boolean flag = true;@Overridepublic void run() {//2.线程体使用该标识int i = 0;while (flag){System.out.println("run....Thread" + i++);}}//3.设置一个公开的方法停止线程,转换你标志位(对外提供方法改变标识)public void stop(){this.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("主线程" + i);if (i == 900){//调用JDK中的stop方法,不是调用上面自己写的stop方法testStop.stop();System.out.println("线程该停止了");}}}
}
线程休眠-sleep
sleep(时间)指定当前线程阻塞的毫秒数;
sleep存在异常InterruptedException;
sleep时间达到后线程进入就绪状态;
sleep可以模拟网络延时,倒计时等。
每一个对象都有一个锁,sleep不会释放锁;
线程延时(超时等待,运用sleep方法),模拟抢票,上一个人买到票,sleep延时200ms
package com.kuang.thread;//多个线程同时操作同一个对象。线程不安全
//买火车票的例子,抢票
//模拟网络延时:放大问题的发生性
public class TestSleep implements Runnable{//票数private int ticketNums = 50;@Overridepublic void run() {while (true) {if (ticketNums <= 0) {System.out.println("票已经被抢完了,没有票了");break;//票已经售完,退出循环}System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums + "张票,剩余" + (--ticketNums) + "张票");//模拟延时try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {TestSleep ticket = new TestSleep();new Thread(ticket,"小明").start();new Thread(ticket,"老师").start();new Thread(ticket,"黄牛党").start();}
}
线程延时-sleep
(超时等待,运用sleep方法),模拟倒计时,每输出倒计时一次,延时1s
package com.kuang.thread;//模拟倒计时
public class TestSleep2 {public static void main(String[] args) {try {tenDown();} catch (InterruptedException e) {throw new RuntimeException(e);}}public static void tenDown() throws InterruptedException {int number = 10;while (true){Thread.sleep(1000);System.out.println("倒计时"+ number-- + "秒");if (number <= 0){break;}}}
}
线程延时(超时等待,运用sleep方法),打印当前时间
package com.kuang.thread;import java.text.SimpleDateFormat;
import java.util.Date;//打印时间
public class TestSleep3 {public static void main(String[] args) {//打印当前系统时间Date startTime = new Date(System.currentTimeMillis());//获取系统当前时间System.out.println("打印时间如下:");while (true) {try {Thread.sleep(1000);//间隔1sSystem.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));//时间格式:HH:mm:ssstartTime = new Date(System.currentTimeMillis());//更新当前时间} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
线程礼让-yield
礼让线程,让当前正在执行的线程暂停,但不阻塞
将线程从运行状态转为就绪状态
让CPU重新调度,礼让不一定成功!看CPU心情
如何使用Java中的Thread.yield()
方法来实现线程的礼让。如下代码:
package com.thread;//测试礼让线程
//礼让不一定成功,看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{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程开始执行");Thread.yield();//调用线程的礼让方法System.out.println(Thread.currentThread().getName() + "线程停止执行");}
}
-
在
main
方法中,创建了一个MyYield
对象myYield
。 -
然后创建了两个线程,分别命名为
a
和b
,并将myYield
对象作为Runnable
任务传递给这两个线程。 -
最后,启动这两个线程。
-
MyYield
类实现了Runnable
接口,并重写了run
方法。 -
在
run
方法中:-
首先打印当前线程的名称,并提示线程开始执行。
-
然后调用
Thread.yield()
方法,尝试让当前线程礼让出CPU资源,让其他线程有机会执行。 -
最后打印当前线程的名称,并提示线程停止执行。
-
Thread.yield()
方法
-
Thread.yield()
是一个静态方法,调用它的线程会提示调度器当前线程愿意让出CPU资源,让其他具有相同优先级的线程有机会运行。 -
需要注意的是,
yield()
方法并不保证当前线程一定会让出CPU资源,这取决于操作系统的线程调度器。因此,线程礼让不一定成功。
运行结果
由于yield()
方法的效果不确定,运行结果可能会有多种情况。以下是可能的输出之一:
a线程开始执行
b线程开始执行
a线程停止执行
b线程停止执行
或者:
a线程开始执行
a线程停止执行
b线程开始执行
b线程停止执行
线程合并-Join
Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
可以想象成插队
练习代码:
package com.thread;//Join 合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
//可以想象成插队
public class TestJoin implements Runnable{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("线程vip来了" + i);}}public static void main(String[] args) throws InterruptedException {//启动我们的线程TestJoin testJoin = new TestJoin();Thread thread = new Thread(testJoin);thread.start();//当调用start()方法的时候,vip线程和主线程都开始执行了,此时是并发的//主线程for (int i = 0; i < 500; i++) {if (i == 200){thread.join();//vip插队了}System.out.println("主线程" + i);}}
}
在代码中,vip
线程并不是在main
线程执行到i == 200
时才开始执行的。实际上,vip
线程在thread.start()
被调用时就已经开始执行了。thread.join()
的作用是让main
线程等待vip
线程执行完毕后再继续执行,而不是让vip
线程在某个特定的时间点才开始执行。
当thread.start()
被调用时,vip
线程就开始执行了。此时,vip
线程和main
线程是并发执行的。
main
线程会继续执行它的循环。当i == 200
时,main
线程会调用thread.join()
,这意味着main
线程会暂停执行,直到vip
线程执行完毕。
-
vip
线程在start()
被调用后就开始执行,它会打印从0到99的数字。由于vip
线程和main
线程是并发执行的,所以在main
线程执行到i == 200
之前,vip
线程可能已经执行了一部分或全部。 -
join()
的作用:
当main
线程执行到i == 200
时,它会调用thread.join()
,此时main
线程会等待vip
线程执行完毕。如果vip
线程已经执行完毕,main
线程会立即继续执行。如果vip
线程还在执行,main
线程会等待直到vip
线程完成。
为什么vip
线程不是到200才开始执行?
vip
线程在thread.start()
被调用时就已经开始执行了,而不是在main
线程执行到i == 200
时才开始。join()
的作用是让main
线程等待vip
线程执行完毕,而不是控制vip
线程的启动时间。
要让 vip
线程在 main
线程执行到 i == 200
时才开始执行,可以通过线程同步机制来实现。Java 提供了多种同步工具,比如 CountDownLatch
、CyclicBarrier
或 wait()
/notify()
等。以下是使用 CountDownLatch
的优化代码:
package com.thread;import java.util.concurrent.CountDownLatch;public class TestJoin implements Runnable {private final CountDownLatch startSignal;public TestJoin(CountDownLatch startSignal) {this.startSignal = startSignal;}@Overridepublic void run() {try {// 等待主线程的信号startSignal.await();} catch (InterruptedException e) {e.printStackTrace();}// VIP 线程开始执行for (int i = 0; i < 100; i++) {System.out.println("线程vip来了" + i);}}public static void main(String[] args) throws InterruptedException {// 创建一个 CountDownLatch,初始值为 1CountDownLatch startSignal = new CountDownLatch(1);// 启动 VIP 线程TestJoin testJoin = new TestJoin(startSignal);Thread thread = new Thread(testJoin);thread.start();// 主线程执行for (int i = 0; i < 500; i++) {if (i == 200) {// 发送信号,让 VIP 线程开始执行startSignal.countDown();}System.out.println("main" + i);}}
}
-
CountDownLatch
的作用:-
CountDownLatch
是一个同步工具,允许一个或多个线程等待其他线程完成操作。 -
在这里,
startSignal
的初始值为 1,表示vip
线程需要等待main
线程发送信号后才能继续执行。
-
-
vip
线程的等待:-
在
vip
线程的run()
方法中,调用startSignal.await()
,使vip
线程等待,直到main
线程调用startSignal.countDown()
。
-
-
main
线程的信号发送:-
当
main
线程执行到i == 200
时,调用startSignal.countDown()
,释放vip
线程,使其开始执行。
-
-
线程执行的顺序:
-
vip
线程在启动后会立即进入等待状态,直到main
线程执行到i == 200
时才会开始执行。
-
输出结果
-
main
线程会从 0 开始打印,直到 200。 -
当
i == 200
时,vip
线程开始执行,打印 "线程vip来了"。 -
main
线程和vip
线程会交替执行(具体顺序取决于线程调度)。
其他实现方式
如果不想使用 CountDownLatch
,也可以使用 wait()
和 notify()
来实现类似的功能。以下是使用 wait()
和 notify()
的版本:
package com.thread;public class TestJoin implements Runnable {private final Object lock;public TestJoin(Object lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {try {// 等待主线程的通知lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}// VIP 线程开始执行for (int i = 0; i < 100; i++) {System.out.println("线程vip来了" + i);}}public static void main(String[] args) throws InterruptedException {Object lock = new Object();// 启动 VIP 线程TestJoin testJoin = new TestJoin(lock);Thread thread = new Thread(testJoin);thread.start();// 主线程执行for (int i = 0; i < 500; i++) {if (i == 200) {synchronized (lock) {// 通知 VIP 线程开始执行lock.notify();}}System.out.println("main" + i);}}
}
-
使用
CountDownLatch
或wait()
/notify()
可以精确控制线程的执行顺序。 -
CountDownLatch
更适合这种一次性信号通知的场景,而wait()
/notify()
更适合复杂的线程协作场景。
线程的状态观测
线程状态,线程可以处于以下状态之一
NEW 尚未启动的线程处于此状态
RUNNABLE 在java虚拟机中执行的线程处于此状态
BLOCKED 被阻塞等待监视器锁定的线程处于此状态
WAITING 正在等待另一个线程执行特定动作的线程处于此状态
TIMED WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
TERMINATED 已退出的线程处于此状态
一个线程可以在给定时间点处于一个状态,这些状态是不反映任何操作系统状态的虚拟机状态
练习代码:
package com.thread;/*
线程状态观测
线程状态,线程可以处于以下状态之一
NEW 尚未启动的线程处于此状态
RUNNABLE 在java虚拟机中执行的线程处于此状态
BLOCKED 被阻塞等待监视器锁定的线程处于此状态
WAITING 正在等待另一个线程执行特定动作的线程处于此状态
TIMED WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
TERMINATED 已退出的线程处于此状态
一个线程可以在给定时间点处于一个状态,这些状态是不反映任何操作系统状态的虚拟机状态*/
//观察测试线程的状态
public class TestState {public static void main(String[] args) throws InterruptedException {//采用Lambda表达式Thread thread = new Thread( ()->{for (int i = 0; i < 5; i++) {try {Thread.sleep(1000);//延时1s} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("输出线程结束的状态");});//观察状态Thread.State state = thread.getState();System.out.println(state);//NEW 状态//观察启动后thread.start();//启动线程state = thread.getState();System.out.println(state);//Run 状态while (state != Thread.State.TERMINATED){//只要线程不停止,就一直输出状态Thread.sleep(100);//1s钟更新10次state = thread.getState();//更新线程状态System.out.println(state);//输出更新后的线程状态}}
}
线程优先级
Java中提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
线程的优先级用数字表示,范围从1-10
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.MIN_PRIORITY = 5;
使用以下方式改变或获取优先级
getPriority().setPriority(int xxx)
优先级的设定建议在start()调度前
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度
练习代码:
package com.thread;/*
线程优先级
Java中提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
线程的优先级用数字表示,范围从1-10
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.MIN_PRIORITY = 5;
使用以下方式改变或获取优先级
getPriority().setPriority(int xxx)
优先级的设定建议在start()调度前
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度*///测试线程的优先级
public class TestPriority{public static void main(String[] args) {//主线程默认优先级System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());MyPriority myPriority = new MyPriority();Thread thread1 = new Thread(myPriority);Thread thread2 = new Thread(myPriority);Thread thread3 = new Thread(myPriority);Thread thread4 = new Thread(myPriority);Thread thread5 = new Thread(myPriority);Thread thread6 = new Thread(myPriority);//先设置优先级,再启动thread1.start(); //默认优先级thread2.setPriority(1);//设置优先级为1thread2.start();thread3.setPriority(4);//设置优先级为4thread3.start();thread4.setPriority(Thread.MAX_PRIORITY);//MAX_PRIORITY=10,最大优先级thread4.start();thread5.setPriority(Thread.MIN_PRIORITY);//MIN_PRIORITY=1,最小优先级thread5.start();thread6.setPriority(Thread.NORM_PRIORITY);//NORM_PRIORITY=5,默认优先级thread6.start();}
}class MyPriority implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());}
}
守护(daemon)线程
线程分为用户线程和守护线程
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
如,后台记录操作日志,监控内存,垃圾回收等待..
package com.thread;/*
守护(daemon)线程
线程分为用户线程和守护线程
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
如,后台记录操作日志,监控内存,垃圾回收等待..*///测试守护线程
//上帝守护你
public class TestDaemon {public static void main(String[] args) {God god = new God();You you = new You();Thread thread = new Thread(god);thread.setDaemon(true);//默认是false,表示是用户线程,正常的线程都是用户线程thread.start();//启动上帝线程new Thread(you).start();//你 用户线程启动}
}//上帝,永生的
class God implements Runnable{@Overridepublic void run() {while (true){System.out.println("上帝保佑着你!");}}
}//你,一生36500
class You implements Runnable{@Overridepublic void run() {for (int i = 0; i < 36500; i++) {System.out.println("你一生都开心的活着");}System.out.println("====goodbye world!====");//再见,世界}
}
并发、线程同步、队列和锁
并发:多个线程访问同一个对象
某些线程还想修改这个对象,这时候我们就需要线程同步
线程同步其实就是一种等待机制,多个需要同时访问 此对象 的线程进入这个 对象的等待池
等待前面线程使用完毕,下一个线程再使用
队列和锁
每个对象都有一个锁
形成条件 队列+锁 才能保证我们线程同步的安全性
线程同步:
由于同一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突问题
为了保证数据在方法中被访问时的正确性,在访问时加入 锁机制 synchronized,
当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
1、一个线程持有锁会导致其他所有需要此锁的线程挂起;
2、在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题;
3、如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置,引起性能问题
同步方法:
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,
这套机制就是synchronized关键字,它包括两种用法:
synchronized方法和synchronized块
同步方法:public synchronized void method(int args){}
synchronized方法控制“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得
调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才
释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized将会影响效率
同步方法弊端
只读 方法里面需要修改的内容才需要锁,锁的太多,浪费资源
A代码
修改
B代码
同步块
同步块:synchronized(Obj ){}
Obj称之为同步监视器
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class[反射中讲解]
同步监视器的执行过程
1.第一个线程访问,锁定同步监视器,执行其中代码,
2.第二个线程访问,发现同步监视器被锁定,无法访问,
3.第一个线程访问完毕,解锁同步监视器
4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问
多线程模拟抢票
实现了一个多线程模拟买票的程序,在多线程环境下,多个线程同时访问和修改共享资源(票数)时,可能出现线程安全问题。
package com.syn;/*
并发:多个线程访问同一个对象
某些线程还想修改这个对象,这时候我们就需要线程同步
线程同步其实就是一种等待机制,多个需要同时访问 此对象 的线程进入这个 对象的等待池
等待前面线程使用完毕,下一个线程再使用队列和锁
每个对象都有一个锁
形成条件 队列+锁 才能保证我们线程同步的安全性线程同步:
由于同一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突问题
为了保证数据在方法中被访问时的正确性,在访问时加入 锁机制 synchronized,
当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:一个线程持有锁会导致其他所有需要此锁的线程挂起;在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题;如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置,引起性能问题*///不安全的买票
//线程不安全
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 ticketNumbers = 10;boolean flag = true;//定义一个标志位,用来控制循环,外部停止方式@Overridepublic void run() {//买票while (flag){try {buy();//调用买票方法} catch (InterruptedException e) {e.printStackTrace();}}}//买票方法private void buy() throws InterruptedException {//判断是否有票,如果票数小于等于0,就是没票了if (ticketNumbers <= 0) {flag = false;return;}//模拟延时Thread.sleep(100);//买票System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNumbers-- + "张票");}
}
1. 代码功能
-
模拟买票:程序模拟了多个用户(“苦逼的我”、“牛逼的你们”、“可恶的黄牛党”)同时从一个共享的票池中抢票的过程。
-
线程安全问题:由于多个线程同时访问和修改共享资源(票数),可能会导致数据不一致的问题。例如,多个线程可能同时读取到相同的票数,导致同一张票被多个线程抢到。
2. 代码结构
-
UnsafeBuyTicket
类:这是程序的入口类,包含main
方法。在main
方法中,创建了一个BuyTicket
对象,并启动了三个线程来模拟多个用户抢票的过程。 -
BuyTicket
类:实现了Runnable
接口,表示买票操作。该类包含以下属性:-
ticketNumbers
:票的总数,初始值为10。 -
flag
:一个布尔标志位,用于控制买票循环的结束。
BuyTicket
类实现了Runnable
接口的run
方法,并在run
方法中调用了buy
方法来实现买票逻辑。 -
3. 代码实现
-
UnsafeBuyTicket
类的main
方法:-
创建一个
BuyTicket
对象。 -
启动三个线程,分别代表“苦逼的我”、“牛逼的你们”、“可恶的黄牛党”,并传入
BuyTicket
对象作为Runnable
任务。
-
-
BuyTicket
类的run
方法:-
使用一个
while
循环不断调用buy
方法,直到票被抢完(flag
变为false
)。
-
-
BuyTicket
类的buy
方法:-
检查票数是否大于0。如果票数小于等于0,将
flag
设置为false
,结束循环。 -
使用
Thread.sleep(100)
模拟买票操作的延迟,放大线程安全问题。 -
输出当前线程抢到的票号,并将票数减1。
-
4. 线程安全问题
-
问题描述:
-
由于
buy
方法没有使用任何同步机制,多个线程可能会同时访问和修改ticketNumbers
,导致数据不一致。例如,多个线程可能同时读取到相同的票数,导致同一张票被多个线程抢到。 -
输出结果可能会出现重复的票号,或者票号顺序混乱。
-
-
如何解决:
-
可以通过使用
synchronized
关键字或ReentrantLock
等同步机制来确保同一时间只有一个线程可以访问和修改共享资源。
-
采用线程同步synchronized
关键字优化后代码:
package com.kuang.syn;//买票
//synchronized关键字实现线程安全
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 ticketNumbers = 10;boolean flag = true;//定义一个标志位,用来控制循环,外部停止方式@Overridepublic void run() {//买票while (flag){try {buy();//调用买票方法} catch (InterruptedException e) {e.printStackTrace();}}}//买票方法,synchronized 同步方法,锁的是thisprivate synchronized void buy() throws InterruptedException {//判断是否有票,如果票数小于等于0,就是没票了if (ticketNumbers <= 0) {flag = false;return;}//模拟延时Thread.sleep(1000);//买票System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNumbers-- + "张票");}
}
模拟银行取款
采用多线程的方法实现了一个简单的银行取款模拟程序:
package com.syn;//不安全的取钱
//两个人去银行取钱,账户
public class UnsafeBank {public static void main(String[] args) {//账户Account account = new Account(100,"结婚基金");Drawing you = new Drawing(account,50,"你");Drawing girlFriend = new Drawing(account,100,"girlFriend");you.start();girlFriend.start();}
}//账户
class Account{int money;//金额String name;public Account(int money, String name){this.money = money;this.name = name;}
}//银行:模拟取款
class Drawing extends Thread{Account account;//账户//取了多少钱int drawingMoney;//现在手里有多少钱int nowMoney;public Drawing(Account account,int drawingMoney,String name){super(name);this.account = account;this.drawingMoney = drawingMoney;}//取钱@Overridepublic void run() {//判断有没有钱if (account.money-drawingMoney<0){System.out.println(Thread.currentThread().getName() + "银行卡里钱不够,取不了");return;}//sleep可以放大问题的发生性,延时1stry {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//卡内余额 = 余额 - 你取的钱account.money = account.money - drawingMoney;//你手里的钱nowMoney = nowMoney + drawingMoney;System.out.println(account.name + "余额为:" + account.money);//Thread.currentThread().getName = this.getName()System.out.println(this.getName() + "手里的钱:" + nowMoney);}
}
在多线程环境下,多个线程同时访问和修改共享资源(账户余额)时可能出现的线程安全问题。
1. 代码功能
-
模拟银行取款:程序模拟了两个用户(“你”和“girlFriend”)同时从一个共享账户中取款的过程。
-
线程安全问题:由于多个线程同时访问和修改共享资源(账户余额),可能会导致数据不一致的问题。虽然代码中没有显式的线程同步机制,但通过
Thread.sleep(1000)
放大了问题的发生性,使得线程安全问题更容易被观察到。
2. 代码结构
-
UnsafeBank
类:这是程序的入口类,包含main
方法。在main
方法中,创建了一个Account
对象(账户),并启动了两个Drawing
线程(“你”和“girlFriend”)来模拟取款操作。 -
Account
类:表示银行账户,包含两个属性:-
money
:账户余额。 -
name
:账户名称。
该类有一个构造方法,用于初始化账户的余额和名称。
-
-
Drawing
类:继承自Thread
类,表示取款操作。每个Drawing
对象代表一个取款线程。该类包含以下属性:-
account
:要取款的账户。 -
drawingMoney
:要取款的金额。 -
nowMoney
:当前线程(用户)手中的钱。
Drawing
类重写了Thread
类的run
方法,在run
方法中实现了取款逻辑。 -
3. 代码实现
-
UnsafeBank
类的main
方法:-
创建一个
Account
对象,初始余额为100,名称为“结婚基金”。 -
创建两个
Drawing
线程对象,分别代表“你”和“girlFriend”,并启动这两个线程。
-
-
Drawing
类的run
方法:-
首先检查账户余额是否足够取款。如果余额不足,输出提示信息并返回。
-
使用
Thread.sleep(1000)
模拟取款操作的延迟,放大线程安全问题。 -
更新账户余额和当前线程手中的钱。
-
输出账户余额和当前线程手中的钱。
-
4. super
和this
关键字的作用
-
super
关键字:-
在
Drawing
类的构造方法中,super(name)
调用了父类Thread
的构造方法,并将线程名称传递给父类。这样,每个Drawing
线程对象都会有一个名称,可以通过this.getName()
获取。
-
-
this
关键字:-
在
Drawing
类的构造方法中,this.account = account
和this.drawingMoney = drawingMoney
用于将传入的参数赋值给当前对象的属性。 -
在
run
方法中,this.getName()
用于获取当前线程的名称。
-
5. 线程安全问题
-
问题描述:由于
Drawing
类的run
方法中没有使用任何同步机制,两个线程可能会同时访问和修改account.money
,导致数据不一致。例如,两个线程可能同时读取到相同的余额,然后各自减去取款金额,导致最终的余额计算错误。 -
如何解决:可以通过使用
synchronized
关键字或ReentrantLock
等同步机制来确保同一时间只有一个线程可以访问和修改共享资源。
采用synchronized同步块
优化后代码:
package com.kuang.syn;//使用synchronized同步代码块安全的取钱
//两个人去银行取钱,账户
public class UnsafeBank {public static void main(String[] args) {//账户Account account = new Account(100,"结婚基金");Drawing you = new Drawing(account,50,"你");Drawing girlFriend = new Drawing(account,100,"girlFriend");you.start();girlFriend.start();}
}//账户
class Account{int money;//金额String name;public Account(int money, String name){this.money = money;this.name = name;}
}//银行:模拟取款
class Drawing extends Thread{Account account;//账户//取了多少钱int drawingMoney;//现在手里有多少钱int nowMoney;public Drawing(Account account,int drawingMoney,String name){super(name);this.account = account;this.drawingMoney = drawingMoney;}//取钱//synchronized默认锁的是this(这个类),synchronized同步块可以锁任何东西@Overridepublic void run() {//锁的对象就是变化的量,需要增删改查的对象synchronized (account){//判断有没有钱if (account.money-drawingMoney<0){System.out.println(Thread.currentThread().getName() + "银行卡里钱不够,取不了");return;}//sleep可以放大问题的发生性,延时1stry {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//卡内余额 = 余额 - 你取的钱account.money = account.money - drawingMoney;//你手里的钱nowMoney = nowMoney + drawingMoney;System.out.println(account.name + "余额为:" + account.money);//Thread.currentThread().getName = this.getName()System.out.println(this.getName() + "手里的钱:" + nowMoney);}}
}
线程不安全的集合代码:
package com.syn;import java.util.ArrayList;
import java.util.List;//线程不安全的集合
public class UnsafeList {public static void main(String[] args) throws InterruptedException {List<String> list = new ArrayList<String>();for (int i = 0; i < 10000; i++) {new Thread( ()->{list.add(Thread.currentThread().getName());}).start();}Thread.sleep(50);System.out.println(list.size());}
}
使用synchronized同步块优化后代码:
package com.kuang.syn;import java.util.ArrayList;
import java.util.List;//线程安全的集合
public class UnsafeList {public static void main(String[] args) throws InterruptedException {List<String> list = new ArrayList<String>();for (int i = 0; i < 10000; i++) {new Thread( ()->{synchronized (list){list.add(Thread.currentThread().getName());}}).start();}Thread.sleep(50);System.out.println(list.size());}
}
测试JUC安全类型的集合
package com.kuang.syn;import java.util.concurrent.CopyOnWriteArrayList;//测试JUC安全类型的集合
public class TestJUC {public static void main(String[] args) {CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<String>();for (int i = 0; i < 10000; i++) {new Thread(()->{copyOnWriteArrayList.add(Thread.currentThread().getName());}).start();}try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(copyOnWriteArrayList.size());}
}