线程安全

一、概述:

多线程同时共享同一个全局变量或静态变量做写的操作时,会发生数据冲突问题,也就是线程安全问题。

二、同步机制:

同步机制是Java为了解决线程安全问题引入的机制,该机制利用锁的概念,使多线程共享的数据只让一个线程进行读写操作,当前线程执行完成后释放锁,然后才让其他线程进行读写操作。Java中的锁主要可分为内置锁和显示锁。

1. 内置锁:

每个Java对象都可以看作实现同步的锁,称为内置锁,又称互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁。内置锁使用synchronized关键字实现,synchronized关键字有两种用法:

(1)同步代码块:

格式:
synchronized(锁对象){
    可能出现线程安全的代码(访问共享数据)
}
//同步代码块中的锁对象,可以是任意的Java对象
//为了保证多线程同步,使用的锁对象必须是同一个
示例:
public class Ticket implements Runnable{ 
    private int ticket = 100;           
    Object lock = new Object();      
       
    @Override      
    public void run() {      
    //每个窗口卖票的操作           
    //窗口 永远开启           
    while(true){          
        synchronized (lock) {              
            if(ticket>0){
                //有票 可以卖                  
                //出票操作                      
                //使用sleep模拟一下出票时间                       
                try {                      
                    Thread.sleep(50);                          
                } catch (InterruptedException e) {                                               
                    e.printStackTrace();                          
                }                      
                //获取当前线程对象的名字
                String name = Thread.currentThread().getName();                                                    
                System.out.println(name+"正在卖:"+ticket‐‐);                      
            }  
        }
    }
}                

(2)同步方法:
步骤:
①把访问共享数据的代码抽取出来,放到一个方法中;
②在方法中添加synchronized字符修饰。

格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
    可能出现线程安全的代码(访问共享数据)
}
//同步代码块中的锁对象,可以是任意的Java对象
//为了保证多线程同步,使用的锁对象必须是同一个
示例:
public class Ticket implements Runnable{ 
    private int ticket = 100;           
    
    @Override      
    public void run() {      
        //每个窗口卖票的操作           
        //窗口 永远开启           
        while(true){          
            sellTicket();              
        }          
    }
           
    public synchronized void sellTicket(){              
        if(ticket>0){ 
            //有票 可以卖                
            //出票操作             
            //使用sleep模拟一下出票时间              
            try {                
                Thread.sleep(100);                
            } catch (InterruptedException e) {                                   
                e.printStackTrace();
            }             
            //获取当前线程对象的名字              
            String name = Thread.currentThread().getName();             
            System.out.println(name+"正在卖:"+ticket‐‐);         
        } 
    }      
}

同步方法的锁对象是隐含对象,对于非static方法来说,其锁对象就是该实现类对象(this),而static方法由于其在实现类对象创建之前就已经加载进内存,所以无法获取实现类对象,所以其锁对象是本类的class属性(class文件对象)。

2. 显示锁:

java.util.concurrent.locks.Lock 接口提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

(1)Lock接口中的方法:
①void lock();
②void unlock();

java.util.concurrent.locks.ReentrantLock implements Lock接口,所以我们使用Lock锁时通过创建ReentrantLock对象来获取锁。

(2)步骤:
①在成员位置创建一个ReentrantLock对象;
②在可能出现问题的代码前调用Lock接口中的lock()方法获取锁;
③在可能出现问题的代码后调用Lock接口中的unlock()方法释放锁;

示例:
public class Ticket implements Runnable{ 
    private int ticket = 100;           
    Lock lock = new ReentrantLock();  
    
    @Override      
    public void run() {      
        //每个窗口卖票的操作           
        //窗口 永远开启           
        while(true){          
            lock.lock();              
            if(ticket>0){
                //有票 可以卖              
                //出票操作                   
                //使用sleep模拟一下出票时间                   
                try {                  
                    Thread.sleep(50);                      
                } catch (InterruptedException e) {                                      
                    e.printStackTrace();                      
                }                  
                //获取当前线程对象的名字                   
                String name = Thread.currentThread().getName();                 
                System.out.println(name+"正在卖:"+ticket‐‐);                  
            }              lock.unlock();              
        }          
    }      
}

三、ThreadLocal:

ThreadLocal为线程提供一个局部变量。使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

1. ThreadLocal的接口方法:

• void set(Object value);设置当前线程的线程局部变量的值。
• public Object get();该方法返回当前线程所对应的线程局部变量。
• public void remove();将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
• protected Object initialValue();返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

2. 实现原理:

ThreadLocal 是一个泛型类,保证可以接受任何类型的对象,因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。
Map.put(“当前线程”,值对象)。

示例:
class Res {
    // 生成序列号共享变量
    public static Integer count = 0;
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        protected Integer initialValue() {
            return 0;
        };

    };

    public Integer getNum() {
        int count = threadLocal.get() + 1;
        threadLocal.set(count);
        return count;
    }
}

public class ThreadLocaDemo2 extends Thread {
    private Res res;

    public ThreadLocaDemo2(Res res) {
        this.res = res;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + res.getNum());
        }
    }

    public static void main(String[] args) {
        Res res = new Res();
        ThreadLocaDemo2 threadLocaDemo1 = new ThreadLocaDemo2(res);
        ThreadLocaDemo2 threadLocaDemo2 = new ThreadLocaDemo2(res);
        ThreadLocaDemo2 threadLocaDemo3 = new ThreadLocaDemo2(res);
        threadLocaDemo1.start();
        threadLocaDemo2.start();
        threadLocaDemo3.start();
    }

}

四、多线程的三大特性:

1. 原子性:
一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性就是为了保证数据一致,线程安全。

2. 可见性:
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

JMM

本地私有内存存放的是共享变量的副本,线程操作共享变量,首先操作的是自己本地内存的副本,当同一时刻只有一个线程操作共享变量时,该线程操作完毕本地内存,然后会刷新到主内存,然后主内存会通知另一个线程,进而更新;但是如果同一时刻有多个线程操作共享变量,会来不及更新主内存进而通知其他线程更新变量,就会出现冲突问题。

3. 有序性:
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。