>  기사  >  Java  >  내장 잠금의 Java 동시 개발 샘플 코드 동기화됨

내장 잠금의 Java 동시 개발 샘플 코드 동기화됨

黄舟
黄舟원래의
2017-03-18 10:39:141464검색

요약:

멀티 스레드 프로그래밍에서 스레드 안전성은 가장 중요한 문제 중 하나입니다. 핵심 개념은 정확성, 즉 여러 스레드가 공유 , 가변 데이터는 데이터 손상이나 기타 예상치 못한 결과로 이어지지 않습니다. 모든 동시성 모드는 이 문제를 해결하는 경우 중요한 리소스에 대한 직렬화된 액세스를 사용합니다. Java에서는 동기화된 상호 배타적 액세스를 구현하기 위해 동기화 및 잠금이라는 두 가지 방법이 제공됩니다. 이 기사에서는 특정 사용 시나리오(동기화 메소드, 동기화 코드 블록, 인스턴스 객체 잠금 및 클래스 객체 잠금), 재진입 및 예방 조치를 포함하여 Java 동시성에서 동기화 내장 잠금 적용에 대해 자세히 설명합니다.

1. 스레드 안전성 문제

싱글 스레드에서는 스레드 안전성 문제가 발생하지 않지만, 멀티 스레드 프로그래밍에서는 동일한 공유 리소스와 변수 리소스에 동시에 접근이 가능합니다. , 이 리소스는 변수, 객체, 파일 등이 될 수 있습니다.

  • 두 가지 점에 특히 주의하세요. 공유: 리소스에 동시에 여러 스레드에 액세스할 수 있음을 의미합니다.

  • 변수: 이는 리소스가 수명 동안 수정될 수 있음을 의미합니다. 따라서 여러 스레드가 동시에 이러한 종류의 리소스에 액세스하면 문제가 발생합니다. 각 스레드의 실행 프로세스를 제어할 수 없으므로 개체의 변수 상태에 대한 액세스를 조정하기 위해 동기화 메커니즘을 사용해야 합니다.

데이터 읽기의 예를 들어보세요:

//资源类
class PublicVar {

    public String username = "A";
    public String password = "AA";

    //同步实例方法
    public synchronized void setValue(String username, String password) {
        try {
            this.username = username;
            Thread.sleep(5000);
            this.password = password;

            System.out.println("method=setValue " +"\t" + "threadName="
                    + Thread.currentThread().getName() + "\t" + "username="
                    + username + ", password=" + password);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //非同步实例方法
    public void getValue() {
        System.out.println("method=getValue " + "\t" +  "threadName="
                + Thread.currentThread().getName()+ "\t" + " username=" + username
                + ", password=" + password);
    }
}

//线程类
class ThreadA extends Thread {

    private PublicVar publicVar;

    public ThreadA(PublicVar publicVar) {
        super();
        this.publicVar = publicVar;
    }

    @Override
    public void run() {
        super.run();
        publicVar.setValue("B", "BB");
    }
}

//测试类
public class Test {

    public static void main(String[] args) {
        try {
            //临界资源
            PublicVar publicVarRef = new PublicVar();

            //创建并启动线程
            ThreadA thread = new ThreadA(publicVarRef);
            thread.start();

            Thread.sleep(200);// 打印结果受此值大小影响

            //在主线程中调用
            publicVarRef.getValue();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}/* Output ( 数据交叉 ): 
        method=getValue     threadName=main         username=B, password=AA
        method=setValue     threadName=Thread-0     username=B, password=BB
 *///:~

프로그램 출력에서 ​​볼 수 있듯이 쓰기 작업이 동기화되더라도 일부 오류는 여전히 발생할 수 있습니다. 위에 표시된 더티 읽기와 같은 예기치 않은 상황. 더티 읽기(Dirty Reading)는 읽기 작업을 수행할 때 해당 데이터가 다른 스레드에 의해 부분적으로 수정되어 데이터 교차가 발생하는 경우 발생합니다.

실제로는 스레드 안전성 문제입니다. 즉, 여러 스레드가 동시에 리소스에 액세스할 때 프로그램 실행 결과가 보고 싶은 결과가 아닐 수 있습니다. 여기서는 이 리소스를 중요 리소스라고 합니다. 즉, 여러 스레드가 중요한 리소스(객체, 객체의 속성 , 파일, 데이터베이스 등)에 동시에 액세스하는 경우 스레드 안전성 문제가 발생할 수 있습니다.

그러나 여러 스레드가 메서드를 실행할 때 메서드 내부의 지역 변수는 중요한 리소스가 아닙니다. 이러한 지역 변수는 각 스레드의 전용 스택에 있으므로 공유되지 않으며 스레드 안전을 초래하지 않기 때문입니다. 문제.

2. 스레드 안전 문제를 해결하는 방법

실제로 모든 동시성 모드는 스레드 안전 문제를 해결할 때 중요한 리소스에 대한 직렬화된 액세스를 사용합니다. 즉, 동시에 단 하나의 스레드만이 중요한 리소스에 액세스할 수 있으며 이를 동기식 상호 배타적 액세스라고도 합니다. 즉, 중요한 리소스에 액세스하는 코드 앞에 잠금을 추가하고 다른 스레드가 계속 액세스할 수 있도록 잠금을 해제합니다.

Java에서는 동기식 상호 배타적 액세스를 구현하기 위해 동기화 및 잠금이라는 두 가지 방법이 제공됩니다. 이 기사에서는 주로 동기화 사용에 대해 설명합니다. Lock 사용은 내 다른 블로그 게시물 "Java Concurrency: Lock Framework 세부 설명"에 설명되어 있습니다.

3. 동기화된 동기화 방법 또는 동기화 차단

동기화된 키워드의 사용 방법을 이해하기 전에 먼저 상호 배제 액세스 잠금의 목적을 달성할 수 있는 뮤텍스 잠금 개념을 살펴보겠습니다. 간단한 예로, 중요한 리소스에 뮤텍스 잠금이 추가되면 한 스레드가 중요한 리소스에 액세스하면 다른 스레드는 대기만 할 수 있습니다.

Java에서는 동기화된 키워드를 사용하여 메서드나 코드 블록을 표시할 수 있습니다. 스레드가 개체의 동기화된 메서드를 호출하거나 동기화된 코드 블록에 액세스하면 스레드가 개체의 잠금을 획득합니다. 스레드는 일시적으로 이 메서드에 액세스할 수 없습니다. 이 메서드나 코드 블록이 실행될 때만 이 스레드가 개체의 잠금을 해제하고 다른 스레드가 이 메서드나 코드 블록을 실행할 수 있습니다.

다음 코드에서 두 스레드는 insertData 객체 를 호출하여 데이터를 삽입합니다 :

1) 동기화된 메서드

public class Test {

    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        // 启动线程 1  
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();

        // 启动线程 2
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();
    }  
}

class InsertData {

    // 共享、可变资源
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();

    //对共享可变资源的访问
    public void insert(Thread thread){
        for(int i=0;i<5;i++){
            System.out.println(thread.getName()+"在插入数据"+i);
            arrayList.add(i);
        }
    }
}/* Output: 
        Thread-0在插入数据0
        Thread-1在插入数据0
        Thread-0在插入数据1
        Thread-0在插入数据2
        Thread-1在插入数据1
        Thread-1在插入数据2
 *///:~

실행 결과에 따르면 두 스레드가 insert() 메소드를 동시에 실행하고 있는 것을 확인할 수 있습니다. 그리고 insert() 메소드 앞에 동기화 키워드를 추가하면 실행 결과는 다음과 같습니다.

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();

    public synchronized void insert(Thread thread){
        for(int i=0;i<5;i++){
            System.out.println(thread.getName()+"在插入数据"+i);
            arrayList.add(i);
        }
    }
}/* Output: 
        Thread-0在插入数据0
        Thread-0在插入数据1
        Thread-0在插入数据2
        Thread-1在插入数据0
        Thread-1在插入数据1
        Thread-1在插入数据2
 *///:~

위 출력 결과를 보면 Thread-0이 데이터를 삽입한 후에 Thread-1이 데이터를 삽입하는 것을 알 수 있습니다. 데이터. Thread-0과 Thread-1은 insert() 메서드를 순차적으로 실행합니다. 이것이 동기화된 키워드가 메소드에 대해 수행하는 작업입니다.

단, 다음 세 가지 사항에 주의해야 합니다.

1)当一个线程正在访问一个对象的 synchronized 方法,那么其他线程不能访问该对象的其他 synchronized 方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

2)当一个线程正在访问一个对象的 synchronized 方法,那么其他线程能访问该对象的非 synchronized 方法。这个原因很简单,访问非 synchronized 方法不需要获得该对象的锁,假如一个方法没用 synchronized 关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,

3)如果一个线程 A 需要访问对象 object1 的 synchronized 方法 fun1,另外一个线程 B 需要访问对象 object2 的 synchronized 方法 fun1,即使 object1 和 object2 是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

2) synchronized 同步块

synchronized 代码块类似于以下这种形式:

synchronized (lock){
    //访问共享可变资源
    ...
}

当在某个线程中执行这段代码块,该线程会获取对象lock的锁,从而使得其他线程无法同时访问该代码块。其中,lock 可以是 this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。特别地, 实例同步方法 与 synchronized(this)同步块 是互斥的,因为它们锁的是同一个对象。但与 synchronized(非this)同步块 是异步的,因为它们锁的是不同对象。

比如上面的insert()方法可以改成以下两种形式:

// this 监视器
class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();

    public void insert(Thread thread){
        synchronized (this) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入数据"+i);
                arrayList.add(i);
            }
        }
    }
}

// 对象监视器
class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Object object = new Object();

    public void insert(Thread thread){
        synchronized (object) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入数据"+i);
                arrayList.add(i);
            }
        }
    }
}

从上面代码可以看出,synchronized代码块 比 synchronized方法 的粒度更细一些,使用起来也灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步。

3) class 对象锁

特别地,每个类也会有一个锁,静态的 synchronized方法 就是以Class对象作为锁。另外,它可以用来控制对 static 数据成员 (static 数据成员不专属于任何一个对象,是类成员) 的并发访问。并且,如果一个线程执行一个对象的非static synchronized 方法,另外一个线程需要执行这个对象所属类的 static synchronized 方法,也不会发生互斥现象。因为访问 static synchronized 方法占用的是类锁,而访问非 static synchronized 方法占用的是对象锁,所以不存在互斥现象。例如,

public class Test {

    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread(){
            @Override
            public void run() {
                insertData.insert();
            }
        }.start(); 
        new Thread(){
            @Override
            public void run() {
                insertData.insert1();
            }
        }.start();
    }  
}

class InsertData { 

    // 非 static synchronized 方法
    public synchronized void insert(){
        System.out.println("执行insert");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行insert完毕");
    }

    // static synchronized 方法
    public synchronized static void insert1() {
        System.out.println("执行insert1");
        System.out.println("执行insert1完毕");
    }
}/* Output: 
        执行insert
        执行insert1
        执行insert1完毕
        执行insert完毕
 *///:~

根据执行结果,我们可以看到第一个线程里面执行的是insert方法,不会导致第二个线程执行insert1方法发生阻塞现象。下面,我们看一下 synchronized 关键字到底做了什么事情,我们来反编译它的字节码看一下,下面这段代码反编译后的字节码为:

public class InsertData {
    private Object object = new Object();

    public void insert(Thread thread){
        synchronized (object) {}
    }

    public synchronized void insert1(Thread thread){}

    public void insert2(Thread thread){}
}

내장 잠금의 Java 동시 개발 샘플 코드 동기화됨

从反编译获得的字节码可以看出,synchronized 代码块实际上多了 monitorenter 和 monitorexit 两条指令。 monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个进程对临界资源的访问。对于synchronized方法,执行中的线程识别该方法的 method_info 结构是否有 ACC_SYNCHRONIZED 标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

有一点要注意:对于 synchronized方法 或者 synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

四. 可重入性

一般地,当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于 Java 的内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁时,那么这个请求就会成功。可重入锁最大的作用是避免死锁。例如:

public class Test implements Runnable {

    // 可重入锁测试
    public synchronized void get() {
        System.out.println(Thread.currentThread().getName());
        set();
    }

    public synchronized void set() {
        System.out.println(Thread.currentThread().getName());
    }

    @Override
    public void run() {
        get();
    }

    public static void main(String[] args) {
        Test test = new Test();
        new Thread(test,"Thread-0").start();
        new Thread(test,"Thread-1").start();
        new Thread(test,"Thread-2").start();
    }
}/* Output: 
        Thread-1
        Thread-1
        Thread-2
        Thread-2
        Thread-0
        Thread-0
 *///:~

五. 注意事项

1). 内置锁与字符串常量

由于字符串常量池的原因,在大多数情况下,同步synchronized代码块 都不使用 String 作为锁对象,而改用其他,比如 new Object() 实例化一个 Object 对象,因为它并不会被放入缓存中。看下面的例子:

//资源类
class Service {
    public void print(String stringParam) {
        try {
            synchronized (stringParam) {
                while (true) {
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//线程A
class ThreadA extends Thread {
    private Service service;

    public ThreadA(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.print("AA");
    }
}

//线程B
class ThreadB extends Thread {
    private Service service;

    public ThreadB(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.print("AA");
    }
}

//测试
public class Run {
    public static void main(String[] args) {

        //临界资源
        Service service = new Service();

        //创建并启动线程A
        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();

        //创建并启动线程B
        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();

    }
}/* Output (死锁): 
        A
        A
        A
        A
        ...
 *///:~

出现上述结果就是因为 String 类型的参数都是 “AA”,两个线程持有相同的锁,所以 线程B 始终得不到执行,造成死锁。进一步地,所谓死锁是指:

不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。

b). 锁的是对象而非引用

在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程将同时去竞争该锁对象:

1).若它们将同时竞争同一把锁,则这些线程之间就是同步的;
2).否则,这些线程之间就是异步的。

看下面的例子:

//资源类
class MyService {
    private String lock = "123";

    public void testMethod() {
        try {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " begin "
                        + System.currentTimeMillis());
                lock = "456";
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "   end "
                        + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//线程B
class ThreadB extends Thread {

    private MyService service;

    public ThreadB(MyService service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.testMethod();
    }
}

//线程A
class ThreadA extends Thread {

    private MyService service;

    public ThreadA(MyService service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.testMethod();
    }
}

//测试
public class Run1 {
    public static void main(String[] args) throws InterruptedException {

        //临界资源
        MyService service = new MyService();

        //线程A
        ThreadA a = new ThreadA(service);
        a.setName("A");

        //线程B
        ThreadB b = new ThreadB(service);
        b.setName("B");

        a.start();
        Thread.sleep(50);// 存在50毫秒
        b.start();
    }
}/* Output(循环): 
       A begin 1484319778766
       B begin 1484319778815
       A   end 1484319780766
       B   end 1484319780815
 *///:~

由上述结果可知,线程 A、B 是异步的。因为50毫秒过后, 线程B 取得的锁对象是 “456”,而 线程A 依然持有的锁对象是 “123”。所以,这两个线程是异步的。若将上述语句 “Thread.sleep(50);” 注释,则有:

//测试
public class Run1 {
    public static void main(String[] args) throws InterruptedException {

        //临界资源
        MyService service = new MyService();

        //线程A
        ThreadA a = new ThreadA(service);
        a.setName("A");

        //线程B
        ThreadB b = new ThreadB(service);
        b.setName("B");

        a.start();
        // Thread.sleep(50);// 存在50毫秒
        b.start();
    }
}/* Output(循环): 
       B begin 1484319952017
       B   end 1484319954018
       A begin 1484319954018
       A   end 1484319956019
 *///:~

由上述结果可知,线程 A、B 是同步的。因为线程 A、B 竞争的是同一个锁“123”,虽然先获得运行的线程将 lock 指向了 对象“456”,但结果还是同步的。因为线程 A 和 B 共同争抢的锁对象是“123”,也就是说,锁的是对象而非引用。

六. 总结

用一句话来说,synchronized 内置锁 是一种 对象锁 (锁的是对象而非引用), 作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是 可重入 的。特别地,对于 临界资源 有:

  • 若该资源是静态的,即被 static 关键字修饰,那么访问它的方法必须是同步且是静态的,synchronized 块必须是 class锁;

  • 若该资源是非静态的,即没有被 static 关键字修饰,那么访问它的方法必须是同步的,synchronized 块是实例对象锁;

实质上,关键字synchronized 主要包含两个特征:

  • 互斥性:保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块;

  • 可见性:保证线程工作内存中的变量与公共内存中的变量同步,使多线程读取共享变量时可以获得最新值的使用。

위 내용은 내장 잠금의 Java 동시 개발 샘플 코드 동기화됨의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.