0
点赞
收藏
分享

微信扫一扫

提出锁的原因以及synchronized锁的含义和使用

柠檬果然酸 2022-05-03 阅读 22

目录

提出锁的原因

 synchronized锁(同步锁)

 synchronized修饰方法

JVM和锁

加锁操作

 1.保证了临界区的原子性

 2.synchronized在有限程度上可以保证内存可见性

3.synchronized也可以给代码重排序增加一定的约束


提出锁的原因

为了解决线程安全问题,需要一些机制,目标和JVM沟通,避免线程不安全的问题发生。

常见类的线程安全问题:ArrayList,LinkedList,PriorityQueue.TreeMap,TreeSet,HashMap,HashSet 都不是线程安全的。例如,ArrayList为什么不是线程安全的?——多个线程同时对一个ArrayList对象有修改操作时,结果会出错。从线程不安全的角度看原因:1.有没有共享数据?ArrayList 对象共享array属性和size属性。2.多个线程有没有写操作?有修改操作,array[size]=num;size++;都是修改。3.原子性或者可见性或代码重排序。

为了方便理解:static boolean lock=false;锁有两种状态:锁上状态(locked)和打开状态(unlocked)。

 synchronized锁(同步锁)

1.语法:synchronized(引用->指向的就是被当成锁的对象){请求锁.. 临界区(被锁保护起来的代码)..释放锁}   //JVM针对每个对象都实现了锁的功能

请求锁:请求成功(该锁没有线程持有)继续大括号内代码的执行;请求失败(该锁被线程持有),请求锁的线程会阻塞,直到锁被释放,重新去请求锁。

临界区:加锁中的代码。

static Object lock=new Object();
        static class MyThread extends Thread{
            @Override
            public void run() {
                synchronized (lock){
                    //临界区代码
                    for (int i = 0; i < 10000; i++) {
                        System.out.println("张三");
                    }
                }
                //非临界区代码
                for (int i = 0; i < 10000; i++) {
                    System.out.println("李四");
                }
            }
        }

 synchronized修饰方法

a. synchronized修饰普通方法:视为对“当前对象(使用哪个对象调用这个同步方法)”加锁 -> synchronized(this){...};

b. synchronized修饰静态方法:视为对静态方法所在的类加锁 -> synchronized(类.class){...};

代码演示:

public class Main {
    synchronized void m1() {
        //同步方法
        //视为对“当前对象(使用哪个对象调用这个同步方法)”加锁
    }

    synchronized static void m2() {
        //同步静态方法
        //视为对静态方法所在的类加锁
    }

    void m3() {
    }

    void m4() {
        synchronized (this) {
        }
    }

    void m5() {
        synchronized (Main.class) {
        }
    }

    Object o1 = new Object();

    void m6() {
        synchronized (o1) {
        }
    }

    static Object o2 = new Object();

    void m7() {
        synchronized (o2) {
        }
    }

    public static void main(String[] args) {
//        s1=new Main();
//        s1=new Main();
//        s3==s1;
        /**
         *     t1线程                               t2线程                 是否互斥
         * s1.m1():加锁,this==s1           s1.m1():加锁,this==s1           互斥
         * s1.m1():加锁,this==s1,s1==s3    s3.m1():加锁,this==s3           互斥
         * s1.m1():加锁                     s2.m1():加锁,s2                 不互斥s1!=s2
         * s1.m1():加锁,this==s1           s1.m2():加锁,Main.class          不互斥
         * s1.m1():加锁,s1指向的对象         s1.m3():没有加锁                  不互斥
         * s1.m1():加锁,s1指向的对象         s1.m4():加锁                     互斥
         * s1.m1():加锁,s1指向的对象         s3.m4():加锁                     互斥
         * s1.m1():加锁,s1指向的对象         s1.m5():加锁,静态方法所在类        不互斥
         * s1.m2():加锁,静态方法所在类        s1.m5():加锁,静态方法所在类         互斥
         * s1.m1():加锁,s1指向的对象         s1.m6():加锁,o1对象               不互斥
         * s1.m6():加锁,s1.o1对象           s2.m6(): 加锁,s2.o1对象           不互斥
         * s1.m6():加锁,s1.o1对象           s3.m6():加锁,s3.o1对象,s3和s1指向同一个对象  互斥
         * s1.m2():加锁,Main.class对象      s1.m7():加锁,Main.o2对象          不互斥
         * s1.m7():加锁,Main.o2对象         s1.m7():加锁,Main.o2对象           互斥
         * s1.m7():加锁,Main.o2对象         s3.m7():加锁,Main.o2对象           互斥
         * s1.m7():加锁,Main.o2对象        s2.m7():加锁,Main.o2对象            互斥
         */
    }
}

JVM和锁

JVM在设计对象时,在对象内部都有一个moniter(监视器),每个对象都有。JVM就可以把每个对象都当成锁使用。这个设计其实不太好,synchronized(引用){...}当引用==null时,隐含一个解引用的操作(dereference)的操作(通过引用操作引用指向的对象)。所以如果为空,会报空指针异常(NullPointerExpection)。

加锁操作

使得线程互斥(synchronized和我们配合,正确使用synchronized),保证线程安全。

public class Main {
    // 这个对象用来当锁对象
    static Object lock = new Object();

    static class MyThread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 1_0000; i++) {
                    System.out.println("我是张三");
                }
            }
        }
    }

    static class MyThread2 extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 1_0000; i++) {
                    System.out.println("我是李四");
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        t1.start();


        Thread t2 = new MyThread2();
        t2.start();
    }
}

 1.保证了临界区的原子性

以下两种加锁操作,加锁的“粒度”不同,第一种加锁粒度较细,第二种加锁粒度较粗,粒度不是越细或者越粗越好,是一个需要工程测量的取值,粗略的说:加锁粒度越细,并发可能性越高。粒度:临界区的代码多少。

           for (int i = 0; i < COUNT; i++) {
                //粒度较细  加锁r++释放锁(循环COUNT次)
                synchronized (sync.class){
                    r++;
                }
            } 
            //粒度较粗   加锁 r++(循环COUNT次) 释放锁
            synchronized (sync.class){
                for (int i = 0; i < COUNT; i++) {
                    r--;
                }
            }

注意:只要加锁就要加锁失败的概率,线程阻塞。 而且只能保证r++和r--互斥,不能保证先后顺序。

下面这种写法是做不到互斥的(原子性的保证),不是一把锁,t1线程加的t1指向的对象,t2线程加的t2指向的对象:

    static class Add extends Thread {
        @Override
        //同步锁 this指向 this->Add线程对象
        public synchronized void run() {
            for (int i = 0; i < COUNT; i++) {
                r++;
            }
        }
    }

    static class Sub extends Thread {
        @Override
        //同步锁 this指向 this->Add线程对象
        public synchronized void run() {
            for (int i = 0; i < COUNT; i++) {
                r--;
            }
        }
    }

 2.synchronized在有限程度上可以保证内存可见性

加锁-临界区代码-解锁:加锁成功之前,清空当前线程的工作内存。临界区代码读某些变量时(主内存上的数据时),保证读到的是最新的(“最新”是有限度的最新)。解锁时,保证把工作内存中的数据全部同步回主内存。

但是,临界区期间的数据读写,不做保证:1.可能读的数据,再次被别的线程更改了,就看不到了。2.期间释放有数据同步回主内存。

所以synchronized在“有限程度上,保证内存可见性。

3.synchronized也可以给代码重排序增加一定的约束

s1;s2;s3;加锁;s4;s5;s6;解锁;s7;s8;s9;

s1;s2;s3;之间不做保证,s4;s5;s6之间不做保证,s7;s8;s9;之间不做保证。

但可以保证的是:s4;s5;s6不会被重排序到加锁前面,也不会重排序到解锁之后。s1;s2;s3也不会重排序到加锁之后,s7;s8;s9;不会重排序到解锁之前。

举报

相关推荐

0 条评论