day24 Java 线程安全


I know, i know
地球另一端有你陪我




一、线程安全

假设我们此时需要模拟某电影院售票过程
此处使用 Runnable 接口的方法

/*
		共有100张票
		分为三个窗口
		每次打印前需要延迟
*/
public class TicketRunnable1 implements Runnable {
    
    
    private static int ticket = 100;
    @Override
    public void run() {
    
    

        while(ticket > 0){
    
    
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(
                    Thread.currentThread().getName() + "正在售出第"
                            + (ticket--)+"张票");
        }
    }
}
    TicketRunnable1 ticketRunnable1 = new TicketRunnable1();

    Thread thread1 = new Thread(ticketRunnable1);
    Thread thread2 = new Thread(ticketRunnable1);
    Thread thread3 = new Thread(ticketRunnable1);

    thread1.setName("小六");
    thread2.setName("fgh");
    thread3.setName("韭菜盒子");

    thread1.start();
    thread2.start();
    thread3.start();
 运行结果中会出现大量重复结果甚至为第 0 张票
 此即为线程安全

判断一个程序是否存在线程安全的标准,三者缺一不可
1、是否存在多线程环境
2、是否存在共享数据/共享变量
3、是否有多条语句操作着共享数据/共享变量


二、如何解决线程安全

1、利用同步代码块

格式:

	synchronized(对象){
    
    
		需要同步的代码;
	}

上述问题存在于 sleep() 后,提高了同时出发输出语句的可能性
因此可修改为

public class synchronized1 implements Runnable{
    
    
    private static int i = 1;
    @Override
    public void run() {
    
    
        while(true){
    
    
        		// 代码块需要一个不变对象
            synchronized(this){
    
    
                try {
    
    
                    Thread.sleep(50);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                if(i <= 100){
    
    
                    System.out.println(Thread.currentThread().getName()
                    + "正在售出第" + i + "张票");
                i++}
            }
        }
    }
}

一次只允许一个线程进入代码块,结束一次运行后,再次进行抢占
本质是将原本的多线程退化为单线程,降低效率

注意事项

错误写法

错误写法

	//	方式一
	while (ticket <= 100) {
    
    
	    synchronized(this){
    
    
	        System.out.println(
	                Thread.currentThread().getName() 
	                + "正在售出第" + (ticket--)+"张票");
    	}
    }
    //	方式二
    synchronized (this) {
    
    
        while (ticket <= 100) {
    
    
            System.out.println(
                    Thread.currentThread().getName() 
                    + "正在售出第" + (ticket--) + "张票");
        }
    }

方式一会导致多个线程进入到循环,最后结果导致数据溢出
方式二会导致只有一个线程进入循环,运行直到循环结束

引用对象的统一

  正常情况下:锁对象是任意对象
  同步方法:锁对象是this
  同步静态方法:锁对象是当前类的字节码文件对象
public class Synchronized2 implements Runnable{
    
    

    private static int i = 1;
    @Override
    public void run() {
    
    

        while(true){
    
    
            if(i % 2 ==0){
    
    
                sell();
            }else{
    
    
                //  由于sell()方法是静态同步方法,所以此处对象也需要同步
                //  即Synchronized2.class,否则如同上了两个不同的锁
                synchronized(Synchronized2.class){
    
    
                    if(i <= 500){
    
    
                        System.out.println(Thread.currentThread().getName()+
                                "正在售出第"+ i + "张票");
                    }
                    i++;
                }
            }
        }
    }
    //  此处synchronized 的对象是当前类的字节码文件对象,即Synchronized2.class
    public synchronized static void sell(){
    
    
        if(i <= 500){
    
    
            System.out.println(Thread.currentThread().getName()
            + "正在售出第"+ i + "张票");
        }
        i++;
    }
}

2、利用锁对象 Lock

JDK1.5之后提供了一个新的锁对象Lock

  Lock:(接口)
    具体的子类:ReentrantLock
            void lock() 获得锁
            void unlock() 释放锁
public class Lock1 implements Runnable{
    
    
    private static int i = 1;
	//	创建子类对象
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
    
    
        while(true){
    
    
            lock.lock();
            if(i <= 100){
    
    
                try {
    
    
                    Thread.sleep(50);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + "正在售出第"+ i + "张票");
                i++;
            }
            lock.unlock();
        }
    }
}

效果类似同步代码块


三、等待唤醒 wait – notify

生产者,消费者问题
在实际情况中,一直是又生产者完成生产后,再由消费者进行购买
引入至代码逻辑中
可以理解为先进行数据的生成或更新,再交由代码调用
这就要求多个线程之间,需要达成某种默契,或者说有序
此时,就可以用到等待唤醒机制

此处通过生成和更新对象成员,再读取来模拟
自定义类

class Pokemon {
    
    
    private String name;
    private  int level;
    boolean flag;

    public Pokemon() {
    
    
    }
    get & set
    ...
}

设置成员

public class SetThread1 implements Runnable {
    
    

    Pokemon s;
    private int x = 0;

    SetThread1(Pokemon s) {
    
    
        this.s = s;
    }

    @Override
    public void run() {
    
    
        while (true) {
    
    
            synchronized (s) {
    
    
                if (x <= 200) {
    
    
                    if (s.flag) {
    
    
                        //  数据新鲜,不需要更新
                        try {
    
    
                            s.wait();
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                        s.notify();

                    } else {
    
    
                        //  数据过期,需要更新
                        if (x % 2 == 0) {
    
    
                            s.setName("Eevee");
                            s.setLevel(15);
                            x++;
                        } else {
    
    
                            s.setName("Pikachu");
                            s.setLevel(20);
                            x++;
                        }
                        //  通知get线程
                        s.setFlag(true);
                        s.notify();
                    }
                }
            }
        }
    }
}

读取成员

public class GetThread1 extends Thread {
    
    
    Pokemon s;
    GetThread1(Pokemon s) {
    
    
        this.s = s;
    }
    @Override
    public void run() {
    
    
        while (true) {
    
    
            synchronized (s) {
    
    
                if (s.flag) {
    
    
                    //  数据新鲜,需要获取
                    System.out.println(s.getName() + "---" + s.getLevel());
                    s.setFlag(false);
                    s.notify();
                } else {
    
    
                    //  数据过期,通知set线程更新
                    try {
    
    
                        s.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    s.notify();
                }
            }
        }
    }
}

生成线程

public class PokemonTest {
    
    

    public static void main(String[] args) {
    
    

        Pokemon pokemon = new Pokemon();

        SetThread1 setThread1 = new SetThread1(pokemon);
        GetThread1 getThread1 = new GetThread1(pokemon);

        Thread thread1 = new Thread(setThread1);
        Thread thread2 = new Thread(getThread1);

        thread1.start();
        thread2.start();
    }
}
输出结果:Eevee---15
         Pikachu---20
		 Eevee---15
		 Pikachu---20
		 Eevee---15
		 ...

总结

1、可能出现线程安全的三个条件(需要同时存在)

    1、是否存在多线程环境
    2、是否存在共享数据/共享变量
    3、是否有多条语句操作共享数据/共享变量

2、解决线程的同步安全问题方案
方案一:
同步代码块:
格式:synchronized(锁对象){
需要同步的代码(操作共享数据/共享变量的代码块)
}

        锁对象的使用:
            注意:发生同步安全的多线程之间的锁对象要一致,锁对象唯一
        正常情况下:锁对象是任意对象
        同步方法:锁对象是this
        同步静态方法:锁对象是当前类的字节码文件对象

方案二:
Lock锁,JDK1.5之后出现
lock() 加锁
unlock() 释放锁

3、wait() & notify() & notifyAll()
wait()
线程进入等待阻塞状态

notify()
随机唤醒一个等待阻塞状态的线程,进入同步阻塞状态

notifyAll()
唤醒所有等待阻塞状态的线程,进入同步阻塞状态
请添加图片描述

Guess you like

Origin blog.csdn.net/qq_41464008/article/details/120914128