synchronized的使用(一)

image

多线程简介

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

那么多线程就必定比单线程快吗?答案是不必定,由于多线程存在单线程没有的问题编程

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

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

多线程的意思就是多个线程同时工做,那么多线程之间如何协同合做,这也就是咱们须要解决的线程通讯线程同步问题服务器

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

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();
}
复制代码

运行输出ide

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();
}
复制代码

运行输出ui

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类方法的时候,别的线程是不能调用加了锁的类方法的,可是能够调用没有加同步关键字的方法或者加了同步关键字的实例方法,也就是说每一个类有且仅有11个锁,每一个实例有且仅有1个锁,可是每一个类能够有一个或者多个实例,类的锁和实例的锁不会相互影响,实例之间的锁也不会相互影响。须要注意的是,一个类和一个实例有且仅有一个锁,当这个锁被其余线程占用了,那么别的线程就没法得到锁,只有阻塞等待this

  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修饰的代码,是保证了有序性的。

synchronized与原子性

一样由于synchronized是给共享变量加锁了,以阻塞的机制去同步,在对共享变量进行读/写操做的时候是原子性的。
因此synchronized修饰的代码,是能保证原子性的。

参考

Java并发编程的艺术
内存可见性和原子性:Synchronized和Volatile的比较
java synchronized类锁,对象锁详解(转载)

原文地址:ddnd.cn/2019/03/21/…

相关文章
相关标签/搜索