synchronized的使用(一)

1,610 阅读8分钟
image

多线程简介

在现代计算机中往往存在多个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类方法的时候,别的线程是不能调用加了锁的类方法的,但是可以调用没有加同步关键字的方法或者加了同步关键字的实例方法,也就是说每个类有且仅有11个锁,每个实例有且仅有1个锁,但是每个类可以有一个或者多个实例,类的锁和实例的锁不会相互影响,实例之间的锁也不会相互影响。需要注意的是,一个类和一个实例有且仅有一个锁,当这个锁被其他线程占用了,那么别的线程就无法获得锁,只有阻塞等待

  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/…