JavaSE(十二)
本文主要探讨线程安全相关概念及解决方案。线程安全问题源于多个线程同时操作共享数据(如全局变量、静态变量),表现为资源冲突(如转账案例)。解决方法包括7种同步机制:同步代码块、同步方法、同步锁(ReentrantLock)、volatile变量、ThreadLocal、阻塞队列和原子变量。同时分析了死锁成因(循环等待资源)及避免方法,并介绍了线程通信的wait/notify机制。最后阐述了线程生命周
5、线程安全问题
5.1共享资源的冲突
案例1:
有几个人围着桌子吃蛋糕,当你举起叉子,准备叉起最后一块蛋糕,正当叉子碰到蛋糕的时候才发现它已经被另一个叉子叉起。这类现象就是共享资源(蛋糕)中出现的冲突,而两个人同时用叉子叉同一块蛋糕的过程我们就称为并发。
案例2:
转账案例:

5.2问题分析
线程安全问题都是由全局变量及静态变量引起的。
若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
综上所述,线程安全问题根本原因:
多个线程在操作共享的数据;
5.3问题解决-线程同步
专业:即线程操作共享数据时其他线程不可以参与执行,直到该线程完成操作;
民间:同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说;
为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制:
1)同步代码块(synchronized)
2)同步方法(synchronized)
3)同步锁(ReenreantLock)
4)特殊域变量(volatile)
5)局部变量(ThreadLocal)
6)阻塞队列(LinkedBlockingQueue)
7)原子变量(Atomic)
同步代码块(synchronized):
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
语法:
synchronized(钥匙){
需要同步操作的代码
}
钥匙:钥匙可以是任意类型;多个线程要使用同一把钥匙。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
同步方法(synchronized):
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)。
同步锁(ReentrantLock):
java.util.concurrent.locks.Lock 提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
方法:
public void lock() :加同步锁。
public void unlock() :释放同步锁。
案例:
package com.hg.Thread.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketTest {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket,"窗口-1").start();
new Thread(ticket,"窗口-2").start();
new Thread(ticket,"窗口-3").start();
}
}
class Ticket implements Runnable{
private Integer ticketNum =10;
private Object obj = new Object(); //唯一钥匙
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
sellTicket();
}
}
//方式二:同步方法,此时this是钥匙
private /*synchronized*/ void sellTicket(){
//方式一:同步代码块
//synchronized (obj){ //锁
lock.lock(); //方式三:和同步代码块一样
try {
if (ticketNum > 0){
System.out.println("当前" + Thread.currentThread().getName() + "售票:" + ticketNum--);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
}
}
6、死锁
6.1什么是死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

6.2产生死锁的原因
线程 1 持有r1,等待获取r2;
线程 2 持有r2,等待获取r1;
两个线程都不会释放已持有的锁,且都在等待对方的锁,形成循环等待,最终导致死锁。
6.3怎样避免死锁
避免一个线程同时获取多个锁;
7、线程通讯
7.1为什么要线程通信
多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。
7.2方式
常用方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放cpu资源。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
案例:两个线程交替打印1-100
package com.hg.Thread.Communication;
public class CommTest {
public static void main(String[] args) {
Comm comm = new Comm();
new Thread(comm).start();
new Thread(comm).start();
}
}
class Comm implements Runnable{
private int i = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
this.notify(); //唤醒等待此钥匙的线程
if (i <= 100) {
System.out.println(Thread.currentThread().getName() +": " + i++);
}else {
break;
}
try {
this.wait(); //该线程让出钥匙并进入阻塞(等待)状态(让出钥匙后必须被notify(),否则一直拿不到钥匙)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
8、线程的生命周期

8.1新建
new关键字创建了一个线程之后,该线程就处于新建状态
JVM为线程分配内存,初始化成员变量值
8.2就绪
当线程对象调用了start()方法之后,该线程处于就绪状态
JVM为线程创建方法栈和程序计数器,等待线程调度器调度
8.3运行
就绪状态的线程获得CPU资源,开始运行run()方法,该线程进入运行状态
8.4阻塞
在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中
止自己的执行,进入阻塞状态
8.5死亡
线程会以如下3种方式结束,结束后就处于死亡状态:
run()或call()方法执行完成,线程正常结束。
线程抛出一个未捕获的Exception或Error。
调用该线程stop()方法来结束该线程,该方法容易导致死锁,不推荐使用。
更多推荐


所有评论(0)