Java synchronized同步方法详解

目录
  • 1.synchronized同步方法
  • 2.synchronized方法将对象作为锁
  • 3.多个锁对象
  • 4.如果同步方法内的线程抛出异常会发生什么?
  • 5.静态的同步方法
  • 总结

面试题:

1.如何保证多线程下 i++ 结果正确?

2.一个线程如果出现了运行时异常会怎么样?

3.一个线程运行时发生异常会怎样?

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

(1) 阻塞式的解决方案:synchronized,Lock

(2) 非阻塞式的解决方案:原子变量

synchronized 即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

1. synchronized 同步方法

当使用synchronized关键字修饰一个方法的时候,该方法被声明为同步方法,关键字synchronized的位置处于同步方法的返回类型之前。

public class SafeDemo {
    // 临界区资源
    private static int i = 0;

    // 临界区代码
    public void selfIncrement(){
        for(int j=0;j<5000;j++){
            i++;
        }
    }

    public int getI(){
        return i;
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        SafeDemo safeDemo = new SafeDemo();
        // 线程1和线程2同时执行临界区代码段
        Thread t1 = new Thread(()->{
            safeDemo.selfIncrement();
        });
        Thread t2 = new Thread(()->{
            safeDemo.selfIncrement();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(safeDemo.getI()); // 9906
    }
}

可以发现,当2个线程同时访问临界区的selfIncrement()方法时,就会出现竞态条件的问题,即2个线程在临界区代码段的并发执行结果因为代码的执行顺序不同而导致结果无法预测,每次运行都会得到不一样的结果。因此,为了避免竞态条件的问题,我们必须保证临界区代码段操作具备排他性。这就意味着当一个线程进入临界区代码段执行时,其他线程不能进入临界区代码段执行。

现在使用synchronized关键字对临界区代码段进行保护,代码如下:

public class SafeDemo {
    // 临界区资源
    private static int i = 0;

    // 临界区代码使用synchronized关键字进行保护
    public synchronized void selfIncrement(){
        for(int j=0;j<5000;j++){
            i++;
        }
    }

    public int getI(){
        return i;
    }
}

经过多次运行测试用例程序,累加10000次之后,最终的结果不再有偏差,与预期的结果(10000)是相同的。

在方法声明中设置synchronized同步关键字,保证其方法的代码执行流程是排他性的。任何时间只允许一个线程进入同步方法(临界区代码段),如果其他线程需要执行同一个方法,那么只能等待和排队。

2. synchronized 方法将对象作为锁

定义线程的执行逻辑:

public class ThreadTask {

    // 临界区代码使用synchronized关键字进行保护
    public synchronized void test() {
        try {
            System.out.println(Thread.currentThread().getName()+" begin");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()+" end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

分别创建两个线程,在两个线程的执行体中执行线程逻辑:

public class ThreadA extends Thread {

    ThreadTask threadTask ;

    public ThreadA(ThreadTask threadTask){
        super();
        this.threadTask = threadTask;
    }

    @Override
    public void run() {
        threadTask.test();
    }
}
public class ThreadB extends Thread {
    ThreadTask threadTask ;

    public ThreadB(ThreadTask threadTask){
        super();
        this.threadTask = threadTask;
    }

    @Override
    public void run() {
        threadTask.test();
    }
}

创建一个锁对象,传给两个线程:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ThreadTask threadTask = new ThreadTask();
        ThreadA t1 = new ThreadA(threadTask);
        ThreadB t2 = new ThreadB(threadTask);
        t1.start();
        t2.start();
    }
}

执行结果:

Thread-0 begin
Thread-0 end
Thread-1 begin
Thread-1 end

这里两个线程的锁对象都是threadTask,所以同一时间只有一个线程能拿到这个锁对象,执行同步代码块。另外,需要牢牢记住“共享”这两个字,只有共享资源的写访问才需要同步化,如果不是共享资源,那么就没有同步的必要。

总结:

(1) A线程先持有object对象的锁,B线程如果在这时调用object对象中的synchronized类型的方法,则需等待,也就是同步;

(2) 在方法声明处添加synchronized并不是锁方法,而是锁当前类的对象;

(3) 在Java中只有将对象作为锁,并没有锁方法这种说法;

(4) 在Java语言中,锁就是对象,对象可以映射成锁,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的synchronized同步方法;

(5) 如果在X对象中使用了synchronized关键字声明非静态方法,则X对象就被当成锁;

3. 多个锁对象

创建两个线程执行逻辑ThreadTask对象,即产生了两把锁

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ThreadTask threadTask1 = new ThreadTask();
        ThreadTask threadTask2 = new ThreadTask();
        // 两个线程分别执行两个不同的线程执行逻辑对象
        ThreadA t1 = new ThreadA(threadTask1);
        ThreadB t2 = new ThreadB(threadTask2);
        t1.start();
        t2.start();
    }
}

执行结果:

Thread-0 begin
Thread-1 begin
Thread-0 end
Thread-1 end

test()方法使用了synchronized关键字,任何时间只允许一个线程进入同步方法,如果其他线程需要执行同一个方法,那么只能等待和排队。执行结果呈现了两个线程交叉输出的效果,说明两个线程以异步方式同时运行。

在系统中产生了两个锁,ThreadA的锁对象是threadTask1,ThreadB的锁对象是threadTas2,线程和业务对象属于一对一的关系,每个线程执行自己所属业务对象中的同步方法,不存在锁的争抢关系,所以运行结果是异步的。

synchronized方法的同步锁实质上使用了this对象锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象作为锁(哪个对象调用了带有synchronized关键字的方法,哪个对象就是锁),其他线程只能等待,前提是多个线程访问的是同一个对象。

4. 如果同步方法内的线程抛出异常会发生什么?

public class SafeDemo {
    public synchronized void selfIncrement(){
        if(Thread.currentThread().getName().equals("t1")){
            System.out.println("t1 线程正在运行");
            int a=1;
            // 死循环,只要t1线程没有执行完这个方法,就不会释放锁
            while (a==1){

            }
        }else{
            System.out.println("t2 线程正在运行");
        }
    }
}
public class SafeDemo {
    public synchronized void selfIncrement(){
        if(Thread.currentThread().getName().equals("t1")){
            System.out.println("t1 线程正在运行");
            int a=1;
            while (a==1){
                Integer.parseInt("a");
            }
        }else{
            System.out.println("t2 线程正在运行");
        }
    }
}

执行结果:t2线程得不到执行

t1 线程正在运行

此时,如果我们在同步方法中制造一个异常:

public class SafeDemo {
    public synchronized void selfIncrement(){
        if(Thread.currentThread().getName().equals("t1")){
            System.out.println("t1 线程正在运行");
            int a=1;
            while (a==1){
                Integer.parseInt("a");
            }
        }else{
            System.out.println("t2 线程正在运行");
        }
    }
}

线程t1出现异常并释放锁,线程t2进入方法正常输出,说明出现异常时,锁被自动释放了。

5. 静态的同步方法

在Java世界里一切皆对象。Java有两种对象:Object实例对象和Class对象。每个类运行时的类型信息用Class对象表示,它包含与类名称、继承关系、字段、方法有关的信息。JVM将一个类加载入自己的方法区内存时,会为其创建一个Class对象,对于一个类来说其Class对象是唯一的。Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机调用类加载器中的defineClass方法自动构造的,因此不能显式地声明一个Class对象。

普通的synchronized实例方法,其同步锁是当前对象this的监视锁。如果某个synchronized方法是static(静态)方法,而不是普通的对象实例方法,其同步锁又是什么呢?

public class StaticSafe {
    // 临界资源
    private static int count = 0;
    // 使用synchronized关键字修饰static方法
    public static synchronized void test(){
        count++;
    }
}

静态方法属于Class实例而不是单个Object实例,在静态方法内部是不可以访问Object实例的this引用的。所以,修饰static方法的synchronized关键字就没有办法获得Object实例的this对象的监视锁。

实际上,使用synchronized关键字修饰static方法时,synchronized的同步锁并不是普通Object对象的监视锁,而是类所对应的Class对象的监视锁。

为了以示区分,这里将Object对象的监视锁叫作对象锁,将Class对象的监视锁叫作类锁。当synchronized关键字修饰static方法时,同步锁为类锁;当synchronized关键字修饰普通的成员方法时,同步锁为对象锁。由于类的对象实例可以有很多,但是每个类只有一个Class实例,因此使用类锁作为synchronized的同步锁时会造成同一个JVM内的所有线程只能互斥地进入临界区段。

public class StaticSafe {
    // 临界资源
    private static int count = 0;
    // 对JVM内的所有线程同步
    public static synchronized void test(){
        count++;
    }
}
z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z

所以,使用synchronized关键字修饰static方法是非常粗粒度的同步机制。

通过synchronized关键字所抢占的同步锁什么时候释放呢?一种场景是synchronized块(代码块或者方法)正确执行完毕,监视锁自动释放;另一种场景是程序出现异常,非正常退出synchronized块,监视锁也会自动释放。所以,使用synchronized块时不必担心监视锁的释放问题。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • Java中synchronized关键字修饰方法同步的用法详解

    Java的最基本的同步方式,即使用synchronized关键字来控制一个方法的并发访问. 每一个用synchronized关键字声明的方法都是临界区.在Java中,同一个对象的临界区,在同一时间只有一个允许被访问. 静态方法则有不同的行为.用synchronized关键字声明的静态方法,同时只能够被一个执行线程访问,但是其他线程可以访问这个对象的非静态的synchronized方法.必须非常谨慎这一点,因为两个线程可以同时访问一个对象的两个不同的synchronized方法,即其中一个是静态s

  • java中synchronized(同步代码块和同步方法)详解及区别

     java中synchronized(同步代码块和同步方法)详解及区别 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问题,当有多条线程同时访问共享数据时,如果进行同步,就会发生错误,Java提供的解决方案是:只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他

  • Java多线程synchronized同步方法详解

    1.synchronized 方法与锁对象 线程锁的是对象. 1)A线程先持有 object 对象的 Lock 锁, B线程可以以异步的方式调用 object 对象中的非 synchronized 类型的方法 2)A线程先持有 object 对象的 Lock 锁, B线程如果在这时调用 object 对象中的 synchronized 类型的方法,则需要等待,也就是同步. 2.脏读(DirtyRead) 示例: public class DirtyReadTest { public static

  • java synchronized同步静态方法和同步非静态方法的异同

    java synchronized 详解 synchronized关键字有两种用法,一种是只用于方法的定义中,另外一种是synchronized块,我们不仅可以使用synchronized来同步一个对象变量,你也可以通synchronizedl来同步类中的静态方法和非静态方法. synchronized块的语法如下: public void method() { synchronized(表达式) { } } public void method() { synchronized(表达式) {

  • Java使用synchronized修饰方法来同步线程的实例演示

    Java中可以使用关键字synchronized进行线程同步控制,实现关键资源顺序访问,避免由于多线程并发执行导致的数据不一致性等问题.synchronized的原理是对象监视器(锁),只有获取到监视器的线程才能继续执行,否则线程会等待获取监视器.Java中每个对象或者类都有一把锁与之相关联,对于对象来说,监视的是这个对象的实例变量,对于类来说,监视的是类变量(一个类本身是类Class的对象,所以与类关联的锁也是对象锁).synchronized关键字使用方式有两种:synchronized方法

  • Java synchronized同步方法详解

    目录 1.synchronized同步方法 2.synchronized方法将对象作为锁 3.多个锁对象 4.如果同步方法内的线程抛出异常会发生什么? 5.静态的同步方法 总结 面试题: 1.如何保证多线程下 i++ 结果正确? 2.一个线程如果出现了运行时异常会怎么样? 3.一个线程运行时发生异常会怎样? 为了避免临界区的竞态条件发生,有多种手段可以达到目的. (1) 阻塞式的解决方案:synchronized,Lock (2) 非阻塞式的解决方案:原子变量 synchronized 即俗称的

  • java synchronized用法详解

    Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 二.然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块.

  • Java 多线程实例详解(三)

    本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题. 一.一个典型的Java线程安全例子 public class ThreadTest { public static void main(String[] args) { Account account = new Account("123456", 1000); DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700); Thr

  • Java ShutdownHook原理详解

    ShutdownHook介绍 在java程序中,很容易在进程结束时添加一个钩子,即ShutdownHook.通常在程序启动时加入以下代码即可 Runtime.getRuntime().addShutdownHook(new Thread(){ @Override public void run() { System.out.println("I'm shutdown hook..."); } }); 有了ShutdownHook我们可以 在进程结束时做一些善后工作,例如释放占用的资源,

  • Java字符串拼接详解

    目录 一.“+” 操作符 二.StringBuilder(非线程安全) 三.StringBuffer(线程安全) 四.String 类的 concat 方法 五.String 类的 join 方法 六.StringUtils.join 七.不建议在 for 循环中使用 “+” 进行字符串拼接 总结 String类原生的字符串处理方法short s=1;s=s+1;与short s=1;s+=1;的区别 一.“+” 操作符 “+” 操作符是字符串拼接最常用的方法之一.编译的时候会把 “+” 操作符

  • Java 数据库连接池详解及简单实例

    Java 数据库连接池详解 数据库连接池的原理是: 连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象.使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用.而连接的建立.断开都由连接池自身来管理.同时,还可以通过设置连接池的参数来控制连接池中的初始连接数.连接的上下限数以及每个连接的最大使用次数.最大空闲时间等等.也可以通过其自身的管理机制来监视数据库连接的

  • Java 回调函数详解及使用

    Java 回调函数详解 前言: C语言中回调函数解释: 回调函数(Callback Function)是怎样一种函数呢? 函数是用来被调用的,我们调用函数的方法有两种: 直接调用:在函数A的函数体里通过书写函数B的函数名来调用之,使内存中对应函数B的代码得以执行.这里,A称为"主叫函数"(Caller),B称为"被叫函数"(Callee). 间接调用:在函数A的函数体里并不出现函数B的函数名,而是使用指向函数B的函数指针p来使内存中属于函数B的代码片断得以执行--听

  • java IO 字节流详解及实例代码

    java IO 字节流详解 1.         如何理解输入输出流? 这是我当初在学习Java IO这一块很难理解的一块,输入输出流我们可必须以一个为参照物:我们以内存为参照物,凡是写入内存的我们叫输入流,从内存中写出的我们叫输出流.看下面的示例图 有了这样的一个概念对于我们再学习Java中的IO流我相信就会变得特别简单了. 2.         再看流的分类 流的分类,Java的流分类比较丰富,刚接触的人看了后会感觉很晕.流分类的方式很多: 1.按照输入的方向分,输入流和输出流,输入输出的参

  • java LinkedList类详解及实例代码

    java  LinkedList类详解 LinkedList的特有功能 A:添加功能 public void addFirst(Object e); public void addLast(Object e); B:特有功能 public Object getFirst(); public Object getLast(); C:删除功能 public Object removeFirst(); public Object removeLast(); 实例代码: import java.util

随机推荐