线程安全性

2019-12-20  本文已影响0人  JBryan

1.线程安全性

当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。----《并发编程实战》
多线程并发访问时,得不到正确的结果,就是线程不安全的。
线程不安全举例:

public class UnsafeThread {
    
    public static int count = 0;
    public static void sum() {
        count++;
    }
    
    public static void main(String[] args) throws InterruptedException {
        //循环启动十个线程,每个线程执行100次++操作.
        for(int j=0;j<10;j++) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    for(int i=0;i<1000;i++) {
                        UnsafeThread.sum();
                    }
                }
            };
            thread.start();
        }
        Thread.sleep(1000);
        System.out.println(count);
    }
}

例子中,产生线程不安全问题的原因: num++ 不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,多线程快递切换,有可能两个同一时刻都读取了同一个num值,之后对它进行+1操作,导致线程安全性。

2.Synchronized关键字

原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
内置锁:每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
互斥锁:内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
修饰普通方法:锁住对象的实例。
修饰静态方法:锁住整个类。
修饰代码块: 锁住一个对象 synchronized (lock) 即synchronized后面括号里的内容。

public class SyncDemo {
    
    /**
     * synchronized修饰方法
     * 锁住的是该对象的实例,而不会影响其他实例
     * @throws InterruptedException 
     */
    public synchronized void method() throws InterruptedException {
        System.out.println(Thread.currentThread().getName());
        Thread.sleep(2000); 
    }
    
    
    /**
     * synchronized修饰静态方法
     * 锁住这个类的所有实例
     * @throws InterruptedException
     */
    public synchronized static void method1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName());
        Thread.sleep(2000);
    }
    
    private Object lock = new Object();
    
    /**
     * synchronized修饰代码块
     * 锁住一个对象,此方法内,锁住lock对象
     * @throws InterruptedException 
     * 
     */
    public void method2() throws InterruptedException {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(2000);
        }
    }
    
    public static void main(String[] args) {
        SyncDemo demo1 = new SyncDemo();
//      SyncDemo demo2 = new SyncDemo();
        
        Thread thread = new Thread() {
            @SuppressWarnings("static-access")
            @Override
            public void run() {
                try {
//                  demo1.method1();
                    demo1.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread.start();
        
        Thread thread2 = new Thread() {
            @SuppressWarnings("static-access")
            @Override
            public void run() {
                try {
//                  demo2.method1();
                    demo1.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };  
        thread2.start();    
    }
}

3.Volatile关键字

能且仅能修饰变量。
保证该变量的可见性,volatile关键字仅仅保证可见性,并不保证原子性。
禁止指令重排序。
A、B两个线程同时读取volatile关键字修饰的对象,A读取之后,修改了变量的值,修改后的值,对B线程来说,是可见
使用场景
1:作为线程开关 。
2:单例,修饰对象实例,禁止指令重排序。

4.单例与线程安全

饿汉式:本身线程安全,在类加载的时候,就已经进行实例化,无论之后用不用到。如果该类比较占内存,之后又没用到,就白白浪费了资源。

public class HungerSingleton {

    private static HungerSingleton hungerSingleton = new HungerSingleton();
    
    public static HungerSingleton getInstance() {
        return hungerSingleton;
    }
    //构造方法一定私有化
    private HungerSingleton() {     
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread() {
                @Override
                public void run() {
                    System.out.println(HungerSingleton.getInstance());
                }
            }.start();
        }
    }
}

懒汉式 :最简单的写法是非线程安全的,在需要的时候再实例化。

public class LazySingleton {
    
    //volatile防止JVM指令重排序
    private static volatile LazySingleton lazySingleton = null;
    
    private LazySingleton() {   
    }
    
    /**
     * 非线性安全的,加上synchronized
     * 若加到方法上,太消耗性能
     * @return
     */
    public static LazySingleton getInstance() {
        if(lazySingleton == null) {
            synchronized (LazySingleton.class) {
                if(lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
                //若直接初始化对象,多个线程同时进入if()代码块,等待释放锁之后,重复初始化
//              lazySingleton = new LazySingleton();
            }
        }
        return lazySingleton;
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread() {
                @Override
                public void run() {
                    System.out.println(LazySingleton.getInstance());
                }
            }.start();
        }
    }
}

5.避免线程安全问题

线程安全性问题成因
1.多线程环境
2.多个线程操作同一共享资源
3.对该共享资源进行了非原子性操作
如何避免,打破成因中三点任意一点 :
1.多线程环境--将多线程改单线程(必要的代码,加锁访问)
2.多个线程操作同一共享资源--不共享资源(ThreadLocal、不共享、操作无状态化、不可变)
3.对该共享资源进行了非原子性操作-- 将非原子性操作改成原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的相应的并发工具类)

上一篇下一篇

猜你喜欢

热点阅读