【线程安全】
线程安全 -- 如果有多个线程同时运行,程序每次运行的结果和单线程运行的结果一样,且其他变量的值也和预期的一样。
线程安全产生都是全局变量、静态变量引起的。若每个线程对对全局变量、静态变量只有读操作,而无写操作,一般来说这个全局变量是线程安全的;若有多个线程同时执行写操作,一般需考虑线程同步,否则就有可能影响线程安全。
线程同步机制能够解决上述线程安全问题 -- 1. 同步代码块 2. 同步方法(静态同步方法)
【1. 同步代码块】
/*
* 同步代码块解决线程安全问题
* 同步代码块公式:
* synchronized(任意对象){
* 线程要操作的共享数据
* }
* 任意对象又称:同步锁 、对象监视器 obj
* 作用:保证同步安全性,没有锁的线程不能执行同步代码块,只能等
* 原理:线程遇到同步代码块时,判断代码块的同步锁还有没有
* 1. 若有锁,获取锁,进入同步代码块执行程序 执行完毕后 出了代码块 线程再将锁还回去
* 2. 若没有锁,线程只能等待,不能进入同步代码块
*/
/*
* 同步代码块解决线程安全问题
* 同步代码块公式:
* synchronized(任意对象){
* 线程要操作的共享数据 -- 可能会产生线程安全问题的代码
* }
*/
public class Tickets implements Runnable{
// 定义出售票的数目
private int tickets = 100;
private Object obj = new Object();
public void run(){
while(true){
// 为了保证共享数据的安全,加入同步代码块
synchronized (obj) {
// 对票数进行判断,大于0可以出售,进行变量--操作
if(tickets >0){
try{
Thread.sleep(100); // 此处线程休眠后,会出现线程安全问题。售票数会出现负数
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+"出售第"+tickets--);
}
}
}
}
}
/*
* 多线程售票案例:多线程并发访问同一数据资源
* 本场电影只有100张票,使用多线程模拟窗口同时卖票
* 即三个线程,出售一个票资源
*/
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建Runnable接口实现类对象
Tickets t = new Tickets();
// 创建3个Thread类对象,传递Runnable接口实现类
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t0.start();t1.start();t2.start(); // 运行结果会出现出售 第0张票 和 第 -1 张票 还有出售相同的票数 出现线程安全问题
}
}
【2. 同步方法】
/*
* 采用同步方法形式,解决线程的安全问题 好处:代码简洁
* 步骤:
* 1. 将线程共享数据和同步,抽取到一个方法中去
* 2. 在方法声明上加上同步关键字 synchronized()
* 同步方法中的对象锁,是本类引用对象this
public synchronized void method() {
* 可能会产生线程安全问题的代码
* }
静态同步方法 对象锁是 本类.class (Tickets2.class)
* public static synchronized void method() {
* 可能会产生线程安全问题的代码
* }
*/
public class Tickets2 implements Runnable{
// 定义出售票的数目
private int tickets = 100;
public void run(){
while(true){
payTicket();
}
}
public synchronized void payTicket(){
// 对票数进行判断,大于0可以出售,进行变量--操作
if(tickets >0){
try{
Thread.sleep(100); // 此处线程休眠后,会出现线程安全问题。售票数会出现负数
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+"出售第"+tickets--);
}
}
}
【同步锁使用的弊端】
死锁 -- 当线程任务出现多个同步时(会有多个锁),若同步中出现了同步嵌套,这时就会引发一种现象:程序出现无线等待,这种现象我们称为死锁。
【死锁问题】
// 类LockA 对象充当锁
public class LockA {
// 私有构造方法,类外不能new
private LockA(){}
// 一个静态成员变量 是自己的对象 外界可以通过类名调用静态变量,拿到这个对象
public static final LockA locka = new LockA();
}
//类LockB 对象充当锁
public class LockB {
// 私有构造方法,类外不能new
private LockB(){}
// 一个静态成员变量 是自己的对象 外界可以通过类名调用静态变量,拿到这个对象
public static final LockB lockb = new LockB();
}
// Runnable 接口实现类
// 类LockA、LockB 创建对象锁
public class DeadLock1 implements Runnable {
private int i = 0;
public void run(){
while(true){
if(i % 2 == 0){
// 先进A同步,再进B同步
synchronized (LockA.locka) {
System.out.println("if...Locka");
synchronized (LockB.lockb) {
System.out.println("if...Lockb");
}
}
}else{
// 先进B同步,再进A同步
synchronized (LockB.lockb) {
System.out.println("else...Lockb");
synchronized (LockA.locka) {
System.out.println("else...Locka");
}
}
}
i++;
}
}
}
/*
* 线程的死锁代码实现:
* LockA、LockB类,构造方法私有了不能new,需要通过类名调用静态方法创建对象
*/
public class ThreadDemo1 {
public static void main(String[] args) {
//
DeadLock1 dead = new DeadLock1();
Thread t0 = new Thread(dead);
Thread t1 = new Thread(dead);
t0.start();
t1.start();
/*
if...Locka
if...Lockb
else...Lockb
if...Locka
运行后,程序进入死锁
*/
}
}
【使用Lock接口】
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
* 使用Lock接口,解决线程安全问题
* Lock接口方法:
* 1. lock()获取锁
* 2. unlock()释放锁
* 接口实现类ReentrantLock();
*/
public class TicketsLock1 implements Runnable {
private int tickets = 100;
// 创建Lock接口的实现类对象 ,声明在成员变量位置
private Lock l = new ReentrantLock();
@Override
public void run() {
while(true){
// 调用Lock接口方法lock()获取锁
l.lock();
if(tickets >0){
try{
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"出售第"+tickets--);
}catch(Exception e){
}finally{
// 调用方法unlock() 释放锁
l.unlock();
}
}
}
}
}
【等待与唤醒机制】
线程之间的通信:多个线程在处理同一个资源,处理的任务却不相同。而通过一定的手段使各个线程能够有效的利用资源,这种手段即 ---- 等待唤醒机制。
wait():等待。将正执行的线程释放其执行资格和执行权,并存储到线程池中。
notify():唤醒。唤醒线程池中被wait()的线程。一次唤醒一个,而且是任一的。
notifyAll():唤醒全部。可以将线程池中的所有wait()线程都唤醒。
这些方法定义在Object中, 因为这些方法使用必须要标明所属的锁,而锁又可以是任意对象,能被任意对象调用的方法一定在Object类中。
/*
* 输入线程:对Resource资源对象中的成员变量进行赋值
* 张三 男 wang w 进行轮流赋值
标记:flag为true 说明:赋值完成 ;flag为false 说明:获取值完成
*/
public class InpputDemo1 implements Runnable {
private Resource r ;
public InpputDemo1(Resource r){
this.r = r;
}
public void run(){
int i =0;
while(true){
synchronized (r) {
// 若flag为true,等待
if(r.flag == true){
try{r.wait();}catch(Exception e){}
}
if(i%2 == 0){
r.name = "张三";
r.sex = "男";
}else{
r.name = "wang";
r.sex = "w";
}
// 唤醒输出线程,标记改为true
r.flag = true;
r.notify();
}
i++;
}
}
}
/*
* 输出线程:对Resource资源对象中的成员变量name sex 进行输出
*/
public class OutputDemo1 implements Runnable{
private Resource r ;
public OutputDemo1(Resource r){
this.r = r;
}
public void run(){
while(true){
synchronized (r) {
// 若标记flag 为false,等待
if(!r.flag){
try{r.wait();}catch(Exception e){}
}
System.out.println(r.name+" "+r.sex);
// 将flag改为false,唤醒输入线程
r.flag = false;
r.notify();
}
}
}
}
/*
* 定义资源类Resource
* 成员变量name sex
*/
public class Resource {
public String name;
public String sex;
// 标记:flag为true 说明:赋值完成 ;flag为false 说明:获取值完成
// 线程输入:需不需赋值看flag状态。若flag为true,等待; 若flag为false,进行赋值,然后将flag改为true
// 线程输出:需不需要输出看flag状态。若flag为true,进行输出,然后将flag改为false; 若flag为false,等待
public boolean flag = false;
}
/*
* 使用等待与唤醒完成以下任务:
* 输入线程向Resource中输入name,sex
* 输出线程从Resource中输出name,sex
*
* 1. 若InpputDemo1发现Resource中没有数据时,开始输入, 输入完成唤醒notify() OutputDemo1来输出;若发现有数据就wait()
* 2. 若OutputDemo1发现Resource中没有数据时,就wait();当发现有数据时,就输出,输出完成后唤醒notify() InpputDemo1来输入数据
*/
public class ThreadTestDemo1 {
public static void main(String[] args) {
Resource r = new Resource();
//创建 Runnable接口实现类对象
InpputDemo1 in = new InpputDemo1(r);
OutputDemo1 out = new OutputDemo1(r);
Thread tin = new Thread(in);
Thread tout = new Thread(out);
tin.start();
tout.start();
/*
张三 男
wang w
张三 男
wang w
张三 男
wang w
张三 男
wang w
*/
}
}
【总结】
1. 多线程有哪些实现方案?
a. 继承Thread类
b. 实现Runnable接口
c. 通过线程池,实现Callable接口
2. 同步的实现方式
a. 同步代码块
synchronized(锁对象) {
可能产生线程安全问题的代码
} // 同步代码块的锁对象可以是任意的对象
b. 同步方法
public synchronized void method() {
可能产生线程安全问题的代码
} // 同步方法中的锁对象是 this
静态同步方法
public static synchronized void method() {
可能产生线程安全问题的代码
} // 静态同步方法中的锁对象是 类名.class
3. 启动线程的是什么方法? run() 与start() 方法的区别?
启动线程的方法是start(),
区别:start() 启动线程,并调用线程中的run()方法;run() 执行该线程对象要执行的任务
4. sleep() 和 wait() 方法的区别?
sleep() 释放cpu使用权,不释放锁,休眠时间内不能被唤醒;
wait() 释放cpu使用权, 释放锁,在等待的时间内能唤醒。
5. 为什么wait() notify() notifyAll() 等方法都定义在Object对象中?
因为锁对象可以是任意类型的对象。