44

synchronized的使用(一)

 5 years ago
source link: http://ddnd.cn/2019/03/21/java-synchronized/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

bY7zy2N.jpg!web

在现代计算机中往往存在多个 CPU 核心,而 1CPU 能同时运行一个线程,为了充分利用 CPU 多核心,提高 CPU 的效率,多线程就应时而生了。

那么多线程就一定比单线程快吗?答案是不一定,因为多线程存在单线程没有的问题

  • 上下文切换 :线程从 运行状态 切换到 阻塞状态 或者 等待状态 的时候需要将线程的运行状态保存,线程从 阻塞状态 或者 等待状态 切换到 运行状态 的时候需要加载线程上次运行的状态。线程的运行状态从保存到再加载就是一次上下文切换,而上下文切换的开销是非常大的,而我们知道 CPU 给每个线程分配的时间片很短,通常是几十毫秒(ms),那么线程的切换就会很频繁。
  • 死锁 :死锁的一般场景是,线程 A 和线程 B 都在互相等待对方释放锁,死锁会造成系统不可用。
  • 资源限制的挑战 :资源限制指计算机硬件资源或软件资源限制了多线程的运行速度,例如某个资源的下载速度是 1Mb/s ,资源的服务器带宽只有 2Mb/s ,那么开 10 个线程下载资源并不会将下载速度提升到 10Mb/s

既然多线程存在这些问题,那么我们在开发的过程中有必要使用多线程吗?我们知道任何技术都有它存在的理由,总而言之就是多线程利大于弊,只要我们合理使用多线程就能达到事半功倍的效果。

多线程的意思就是多个线程同时工作,那么多线程之间如何协同合作,这也就是我们需要解决的 线程通信线程同步 问题

  • 线程通信 :线程通信指线程之间以何种机制来交换消息,线程之间的通信机制有两种: 共享内存消息传递 。共享内存即线程通过对共享变量的读写而达到隐式通信,消息传递即线程通过发送消息给对方显示的进行通信。
  • 线程同步 :线程同步指不同线程对同一个资源进行操作时候线程应该以什么顺序去操作,线程同步依赖于线程通信,以共享内存方式进行线程通信的线程同步是显式的,以消息传递方式进行线程通信的线程同步是隐式的。

synchronized简介

synchronized 是Java的关键字,可用于同步实例方法、类方法(静态方法)、代码块

  • 同步实例方法 :当 synchronized 修饰实例方法的时候,同步的范围是当前实例的实例方法。
  • 同步类方法 :当 synchronized 修饰类方法的时候,同步的范围是当前类的方法。
  • 同步代码块 :当 synchronized 修饰代码块的时候,同步的范围是 () 中的对象。

"talk is cheap show me the code" 让我们分别运行个例子来看看。

  1. 同步实例方法
    synchronized public void synSay() {
        System.out.println("synSay----" + Thread.currentThread().getName());
        while (true) { //保证进入该方法的线程 一直占用着该同步方法
    
        }
    }
    
    public void say() {
        System.out.println("say----" + Thread.currentThread().getName());
    }
    public static void main(String[] args){
        Test test1 = new Test();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                test1.synSay();
            }
        });
    
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);  //休眠3秒钟 保证线程t1先执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test1.say();
                test1.synSay();
            }
        });
    
        t1.start();
        t2.start();
    }
    

运行输出

synSay----Thread-0  //线程t1
say----Thread-1  //线程t2

创建 t1t2 两个线程,分别执行同一个实例 test1 的方法,线程 t1 先执行加了同步关键字的 synSay 方法,注意方法里面需要加上个 while 死循环,目的是让线程一直在同步方法里面,然后然线程t1执行之后再让线程t2去执行,此时线程t2并不能成功进入到 synSay 方法里面,因为此时线程t1正在方法里面,线程2只能在 synSay 方法外面阻塞,但是线程t2可以进入到没有加同步关键字的 say 方法。

也就是说 关键字 synchronized 修饰实例方法的时候,锁住的是该实例的加了同步关键字的方法,而没有加同步关键字的方法,线程还是可以正常访问的 。但是不同实例之间同步是不会影响的,因为每个实例都有自己的一个锁,不同实例之间的锁是不一样的。

  1. 同步类方法
    synchronized static public void synSay() {
        System.out.println("static synSay----" + Thread.currentThread().getName());
        while (true) { //保证进入该方法的线程 一直占用着该同步方法
    
        }
    }
    
    synchronized public void synSay1() {
        System.out.println("synSay1----" + Thread.currentThread().getName());
    }
    
    public void say() {
        System.out.println("say----" + Thread.currentThread().getName());
    }
    public static void main(String[] args){
        Test test1 = new Test();
        Test test2 = new Test();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                test1.synSay();
            }
        });
    
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);  //休眠3秒钟 保证线程t1先执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test1.say();
                test2.say();
                test1.synSay();
            }
        });
    
        t1.start();
        t2.start();
    }
    

运行输出

static synSay----Thread-0 //线程t1 实例test1
say----Thread-1 //线程t2 实例test1
say----Thread-1 //线程t2 实例test2

static synSay----Thread-0 //线程t1 实例test1
say----Thread-1  //线程t2 实例test1
synSay1----Thread-1 //线程t2 实例test1
say----Thread-1 //线程t2 实例test2

这里和上面的同步实例方法的代码差不多,就是将 synSay 方法加上了 static 修饰符,即把方法从实例方法变成类方法了,然后我们再新建个实例 test2 ,先让线程t1调用实例test1的synSay类方法,在让线程t2去调用实例test1的say实例方法、synSay类方法和让线程t2去调用实例test2的say实例方法,发现 在线程t1占用加了同步关键字的 synSay 类方法的时候,别的线程是不能调用加了锁的类方法的,但是可以调用没有加同步关键字的方法或者加了同步关键字的实例方法 ,也就是说每个类有且仅有1 1 个锁,每个实例有且仅有 1 个锁,但是每个类可以有一个或者多个实例,类的锁和实例的锁不会相互影响,实例之间的锁也不会相互影响。 需要注意的是,一个类和一个实例有且仅有一个锁,当这个锁被其他线程占用了,那么别的线程就无法获得锁,只有阻塞等待

uaieaeb.jpg!web

  1. 同步代码块
        public void synSay() {
            String x = "";
            System.out.println("come in synSay----" + Thread.currentThread().getName());
            synchronized (x) {
                System.out.println("come in synchronized----" + Thread.currentThread().getName());
                while (true) { //保证进入该方法的线程 一直占用着该同步方法
    
                }
            }
        }
    public static void main(String[] args){
            Test test1 = new Test();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    test1.synSay();
                }
            });
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(3000);  //休眠3秒钟 保证线程t1先执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test1.synSay();
                }
            });
    
            t1.start();
            t2.start();
    }
    

运行输出

come in synSay----Thread-0
come in synchronized----Thread-0
come in synSay----Thread-1

可以发现同步代码块和同步实例方法、同步类方法其实差不多,但是同步代码块将同步的范围缩小了,可以同步到指定的对象上,而不像同步实例方法、同步类方法那样同步的是整个方法,所以 同步代码块 在效率上比其他两者都有较大的提升。

需要注意的是,当同步代码块的时候,在 类方法 中加入同步代码块且同步的对象是 xx.class 等类的引用的时候,同步的是该类,如果在 **实例方法 中加入同步代码块且同步的对象是 this ,那么同步的是该实例,可以看成前者使用的是 类的锁 ,后者使用的是 实例的锁

synchronized的特性

建议把 volatile 的特性和 synchronized 的特性进行对比学习,加深理解。 《Java volatile关键字解析》

synchronized与可见性

JMM 关于 synchronized 的两条语义规定了:

  • 线程加锁前:需要将工作内存清空,从而保证了工作区的变量副本都是从主存中获取的最新值。
  • 线程解锁前;需要将工作内存的变量副本写回到主存中。

大概流程: 清空线程的工作内存->在主存中拷贝变量副本到工作内存->执行完毕->将变量副本写回到主存中->释放锁

所以 synchronized 能保证共享变量的 可见性 ,而实现这个流程的原理也是通过插入内存屏障,和关键字 volatile 相似。

synchronized与有序性

因为 synchronized 是给共享变量加锁,即使用阻塞的同步机制,共享变量只能同时被一个线程操作, 所以 JMM 不用像 volatile 那样考虑加内存屏障去保证 synchronized 多线程情况下的有序性,因为 CPU 在单线程情况下是保证了有序性的

所以 synchronized 修饰的代码,是保证了有序性的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK