美文网首页
线程-2.线程互斥

线程-2.线程互斥

作者: 棱锋 | 来源:发表于2018-09-09 16:41 被阅读0次

    线程互斥可以理解为多线程可能同时对某一资源进行操作,造成紊乱。因此需要给这一资源进行加锁,比如一个马桶,一个人在用时,其他人不能也不该去抢着用,这时就需要进行加锁操作。

    加锁一般有两种方式,synchronized和显示的lock。

    1. synchronized

    synchronized可以用于同步方法或者是同步代码块,用于同步方法时,加锁对象就是方法所属的对象,同步代码块时需要显式指出加锁对象。

    如:

    synchronized(this){}
    

    就是指明当前对象为加锁对象。

    需要注意的是synchronized加锁的目标是对象实例,而不是方法,更不是引用,一个任务可以获得多次锁,一个对象的同步方法(块)A中调用对象的另一个同步方法(块)B,此时某任务运行了方法A,就会获得两次锁(synchronized是可重入锁),只有在锁被完全释放,其他任务才可以使用此对象。

    public synchronized void oneMethod(){
        a++;
        try {
            Thread.sleep(1000*2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        twoMethod();
        a++;
    }
    public synchronized void  twoMethod(){
        a--;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    

    线程互斥的根本在于共享同一资源,所以如果你创建了多个对象,还谈什么同步,错误示例如下:

    public class Wrong {
        private List<String> strings = new ArrayList<>();
        public synchronized void method1(){
            strings.add("aaa");
        }
    
        public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    new Wrong().method1();
                }
            });
            new Thread(new Runnable() {
                @Override
                public void run() {
                    new Wrong().method1();
                }
            });
        }
    }
    

    显然毫无关联,两个线程间不会有什么共享资源的问题,因为压根就是两个对象。

    静态方法

    静态方法前也可以加上synchronized修饰符,用来在类的范围内防止对static数据的并发访问,此时的加锁对象是该类的Class对象。实例中的put方法和print方法的加锁是等价的。

    public class StaticSynchronized {
        private static List<Integer> list = new ArrayList<>();
        public synchronized static void put(){
            for (int i = 0;i < 5; i++){
                list.add(i);
            }
        }
        public static void print(){
            synchronized (StaticSynchronized.class){
                for (int i:list){
                    System.out.print(i + " ");
                }
                System.out.println();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService exec = Executors.newCachedThreadPool();
            for (int i = 0; i < 4; i++){
                exec.execute(new Runnable() {
                    @Override
                    public void run() {
                        StaticSynchronized.put();
                    }
                });
            }
            exec.shutdown();
            Thread.sleep(1000);
            StaticSynchronized.print();
        }
    }   
    

    2 lock

    lock是一个接口,唯一实现类为ReentLock。它需要显式的创建锁这个对象,也需要显式的释放锁,锁的对象就是lock本身。由于他的释放锁是显式的,常常放在finnally中,并且在finnally中做些其他的清理工作(关闭io流等),维持系统稳定。而synchronized中,如果某些事情失败,会直接抛出异常,没有时间清理系统。

    Lock主要有三种创建锁的方式:lock,tryLock,lockInterrputibly

    返回值 方法名(参数)
    void lock() 获取锁。
    void lockInterruptibly() 如果当前线程未被中断,则获取锁。
    Condition newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。
    boolean tryLock() 仅在调用时锁为空闲状态才获取该锁。
    boolean tryLock(long time, TimeUnit unit) 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
    void unlock() 释放锁。

    1. 使用lock

    private int value = 0;
    private Lock lock = new ReentrantLock();
    @Override
    public int next() {
        lock.lock();
        try {
            value++;
            value++;
            return value;
        }finally {
            lock.unlock();
        }
        //return value;
    }
    

    finnally中释放锁,在lock与unclok之间的代码块被加锁。lock需要显式的创建,常常是创建为成员变量,如果在方法体内创建锁实例,就毫无加锁的效果。这是因为lock的锁是客观存在的。如果创建了多个锁实例,那么多个锁实例间,加锁解锁并无关联.错误示例:

    public class WrongLock {
        private ArrayList<Integer> arrayList = new ArrayList<>();
        public static void main(String[] args){
            WrongLock wrongLock = new WrongLock();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    wrongLock.insert(Thread.currentThread());
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    wrongLock.insert(Thread.currentThread());
                }
            }).start();
        }
    
        public void insert(Thread thread){
            Lock lock = new ReentrantLock();
            lock.lock();
            try {
                System.out.println(thread.getName()+ " 得到了锁");
                for (int i = 0; i< 5; i++){
                    arrayList.add(i);
                }
            }finally {
                System.out.println(thread.getName() + "释放了锁");
                lock.unlock();
            }
        }
    
    }
    

    最终结果为

    Thread-0得到了锁

    Thread-1得到了锁

    Thread-0释放了锁

    Thread-1释放了锁

    这个错误与上面的synchronized的那个错误示例本质一样。

    2. tryLock

    tryLock意思是尝试获取锁,如果已经被占用,则返回false,如果未被占用就获取锁。这样可以灵活的利用阻塞时间,如果已经被加锁了就可以去执行其他任务。这是synchronized做不到的。

    public class TryLockTest {
    
        private Lock lock = new ReentrantLock();
        private void method(){
            if (lock.tryLock()){
                try {
                    System.out.println(Thread.currentThread().getName() + " try and get lock, start to do work A");
                    try {
                        Thread.sleep(1000 * 5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " finished work A");
                }finally {
                    lock.unlock();
                }
            }else {
                System.out.println(Thread.currentThread().getName()+ " try but not get lock, start to do work B");
                try {
                    Thread.sleep(1000*2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " finished work B");
            }
        }
    
        public static void main(String[] args) {
            TryLockTest tryLockTest = new TryLockTest();
            new Thread(){
                @Override
                public void run() {
                    tryLockTest.method();
                }
            }.start();
            new Thread(){
                @Override
                public void run() {
                    tryLockTest.method();
                }
            }.start();
    
        }
    }
    

    最终结果:

    Thread-0 try and get lock, start to do work A
    Thread-1 try but not get lock, start to do work B
    Thread-1 finished work B
    Thread-0 finished work A

    需要注意的是,只有if(true)时,才会加锁,最后需要在finnally中释放锁。false时不会加锁。

    3. lockInterruptibly

    如果获取了锁立即返回,如果没有获取锁,当前线程处于休眠状态,直到锁定,或者当前线程被别的线程中断。

    public class lockInterruptiblyTest {
        private Lock lock = new ReentrantLock();
        public void test(){
            try {
                lock.lockInterruptibly();
                try {
                    System.out.println(Thread.currentThread().getName() + " 获取到锁");
                    Thread.sleep(1000 * 5);
                }catch (InterruptedException e){
                    System.out.println("睡眠中断");
                }finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "等待获取锁时被中断");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            lockInterruptiblyTest test = new lockInterruptiblyTest();
            Thread thread1 = new Thread(){
                @Override
                public void run() {
                    test.test();
                }
            };
            Thread thread2 = new Thread(){
                @Override
                public void run() {
                    test.test();
                }
            };
            thread1.start();
            thread2.start();
            Thread.sleep(2*1000);
            thread2.interrupt();
        }
    }
    

    thread2在等待获取锁时(休眠时)被打断。最终结果:

    Thread-0 获取到锁
    Thread-1等待获取锁时被中断

    3. synchronized和lock的区别

    1. 性能区别

    synchronized是托管给JVM执行的,而lock是java写的控制锁的代码,在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍;但是ReetrantLock的性能能维持常态;

    2. 用途区别

    synchronized和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,考虑使用ReentrantLock,特别是遇到下面几种需求的时候。
    A. 某个线程在等待一个锁的控制权的这段时间需要中断
    B. 需要尝试获取锁,获取不到要去做其他事情时。
    C. 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程

    3. 使用区别

    synchronized:
    在需要同步的对象中加入此控制,synchronized可以加在方法上,也加在特定代码块中,括号中表示需要锁的对象

    Lock:
    需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效,锁的对象就是Lock本身。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁

    4. 原子性与可视性

    1. atomicity

    如果一个操作不可分割,那么说他具有原子性
    原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”,比如读取return,赋值=,可以保证他们确实是原子操作。
    但是long和double类型是64位的,其读取写入会被jvm当做分离的32位操作,从而导致不同的任务看到不正确结果的可能性(字撕裂)。如果为long,double使用volatile,就会获得读写的原子性。

    值得注意的是,自增(减)操作在java中绝对不是原子性操作。而且不能依赖原子性来保证线程安全。

    public class AtomicityTest implements Runnable {
        private volatile int i = 0;
        public int getValue() { return i; }
        private synchronized void evenIncrement() { i++; i++; }
        public void run() {
            while(true) {
                evenIncrement();
            }
        }
        public static void main(String[] args) {
            ExecutorService exec = Executors.newCachedThreadPool();
            AtomicityTest at = new AtomicityTest();
            exec.execute(at);
    
            while(true) {
                int val = at.getValue();
                if(val % 2 != 0) {
                    System.out.println(val);
                    System.exit(0);
                }
            }
        }
    }
    

    getValue方法依靠了原子性,但是其不是同步方法,它可能在evenIncrement方法使用一半时getValue。因此其线程并不安全

    2. volatile

    可视性问题是指,多线程访问同一内存时,会创建各自私有备份(缓存),来优化jvm,缓存并不会立即同步到内存,因此其他线程看到的并不一定是真实的数据。

    如果在变量上使用volatile关键字,则会告诉jvm,这个变量不会被线程缓存,而是直接对内存修改。若已经为同步方法(代码块),则必然是可视的。

    3. 原子类

    AtomicInteger,AtomicLong,AtomicReference等特殊的原子性变量类,使用这些类时,不需要再使用同步

    public class AtomicIntegerTest implements Runnable {
        private AtomicInteger integer = new AtomicInteger();
        public int getValue(){
            return integer.get();
        }
        private void evenIncrement(){
            integer.addAndGet(2);
        }
        @Override
        public void run() {
            while (true){
                evenIncrement();
            }
        }
    
        public static void main(String[] args) {
            new Timer().schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("over");
                    System.exit(0);
                }
            }, 5000);
    
            ExecutorService exec = Executors.newCachedThreadPool();
            AtomicIntegerTest ait = new AtomicIntegerTest();
            exec.execute(ait);
            while (true){
                int val = ait.getValue();
                if (val % 2 !=0){
                    System.out.println(val);
                    System.exit(0);
                }
            }
        }
    }
    

    此时getValue和evenIncrement都不是同步方法,但是保证了其安全性。

    5.ThreadLocal

    防止共享资源上产生冲突,ThreadLocal提供另一种解决思路,根除对变量的共享,ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

    这是一种以空间换时间的策略,但是这也是一种“为了防止吃饭噎着,就不吃饭”的策略,其并不解决共享变量的问题,只是回避,如果需要多线程操作同一变量,这种方法明显不可取。

    其在原理上实际是通过Map,键是 Thread,值是它在该 Thread 内的实例。线程通过该 ThreadLocal 的 get() 方案获取实例时,只需要以线程为键,从 Map 中找出对应的实例即可。

    返回值 方法名
    T get() 返回此线程局部变量的当前线程副本中的值。
    protectedT initialValue() 返回此线程局部变量的当前线程的“初始值”。
    void remove() 移除此线程局部变量当前线程的值。
    void set(Tvalue) 将此线程局部变量的当前线程副本中的值设置为指定值。

    initialValue:用于为线程副本初始化。线程第一次使用 get() 方法访问变量时将调用此方法,但如果线程之前调用了 set(T) 方法,则不会对该线程再调用 initialValue 方法

    ThreadLocal在使用时一般作为静态成员。

    错误示例:

    public class ThreadLocalTask implements Runnable{
    
        private static ThreadLocal<Pair> pairThreadLocal = new ThreadLocal<Pair>();
    
        public ThreadLocalTask(Pair pair){
            pairThreadLocal.set(pair);
        }
    
        @Override
        public void run() {
            Pair pair = pairThreadLocal.get();
            pair.incrementY();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            pair.incrementX();
            pair.checkState();
            System.out.println(Thread.currentThread().getName() + pair);
        }
    
        public static void main(String[] args) {
            ExecutorService exec = Executors.newCachedThreadPool();
            Pair pair = new Pair();
            for (int i = 0; i < 5; i++){
                exec.execute(new ThreadLocalTask(pair));
            }
            //System.out.println(pairThreadLocal.get());
            exec.shutdown();
        }
    }
    

    此示例企图通过Task的构造方法来set,但是set方法是为当前线程指定副本,而在new ThreadLocalTask时线程还是main线程,因此一直只是为main线程使用set,其他线程在Run()方法中使用get,都是得到的null(因为并没有复写initialValue)

    正确示例:

    public class ThreadLocalTask implements Runnable{
    
        private static ThreadLocal<Pair> pairThreadLocal = new ThreadLocal<Pair>(){
            @Override
            protected Pair initialValue() {
                return new Pair();
            }
        };
    
        @Override
        public void run() {
            Pair pair = pairThreadLocal.get();
            pair.incrementY();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            pair.incrementX();
            pair.checkState();
            System.out.println(Thread.currentThread().getName() + pair);
        }
    
        public static void main(String[] args) {
            ExecutorService exec = Executors.newCachedThreadPool();
            for (int i = 0; i < 5; i++){
                exec.execute(new ThreadLocalTask());
            }
            //System.out.println(pairThreadLocal.get());
            exec.shutdown();
        }
    }
    

    此时复写了initialValue方法,这样在出此调用get时就会为当前线程创建实例,但是可以发现,其实这很蠢,已经没有任何共享资源可言了。

    相关文章

      网友评论

          本文标题:线程-2.线程互斥

          本文链接:https://www.haomeiwen.com/subject/xsnrgftx.html