멀티 스레드 프로그래밍에서 스레드 안전성은 가장 중요한 문제 중 하나입니다. 핵심 개념은 정확성, 즉 여러 스레드가 공유 , 가변 데이터는 데이터 손상이나 기타 예상치 못한 결과로 이어지지 않습니다. 모든 동시성 모드는 이 문제를 해결하는 경우 중요한 리소스에 대한 직렬화된 액세스를 사용합니다. Java에서는 동기화된 상호 배타적 액세스를 구현하기 위해 동기화 및 잠금이라는 두 가지 방법이 제공됩니다. 이 기사에서는 특정 사용 시나리오(동기화 메소드, 동기화 코드 블록, 인스턴스 객체 잠금 및 클래스 객체 잠금), 재진입 및 예방 조치를 포함하여 Java 동시성에서 동기화 내장 잠금 적용에 대해 자세히 설명합니다.
싱글 스레드에서는 스레드 안전성 문제가 발생하지 않지만, 멀티 스레드 프로그래밍에서는 동일한 공유 리소스와 변수 리소스에 동시에 접근이 가능합니다. , 이 리소스는 변수, 객체, 파일 등이 될 수 있습니다.
두 가지 점에 특히 주의하세요. 공유: 리소스에 동시에 여러 스레드에 액세스할 수 있음을 의미합니다.
변수: 이는 리소스가 수명 동안 수정될 수 있음을 의미합니다. 따라서 여러 스레드가 동시에 이러한 종류의 리소스에 액세스하면 문제가 발생합니다. 각 스레드의 실행 프로세스를 제어할 수 없으므로 개체의 변수 상태에 대한 액세스를 조정하기 위해 동기화 메커니즘을 사용해야 합니다.
데이터 읽기의 예를 들어보세요:
//资源类 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)는 읽기 작업을 수행할 때 해당 데이터가 다른 스레드에 의해 부분적으로 수정되어 데이터 교차가 발생하는 경우 발생합니다.
실제로는 스레드 안전성 문제입니다. 즉, 여러 스레드가 동시에 리소스에 액세스할 때 프로그램 실행 결과가 보고 싶은 결과가 아닐 수 있습니다. 여기서는 이 리소스를 중요 리소스라고 합니다. 즉, 여러 스레드가 중요한 리소스(객체, 객체의 속성 , 파일, 데이터베이스 등)에 동시에 액세스하는 경우 스레드 안전성 문제가 발생할 수 있습니다.
그러나 여러 스레드가 메서드를 실행할 때 메서드 내부의 지역 변수는 중요한 리소스가 아닙니다. 이러한 지역 변수는 각 스레드의 전용 스택에 있으므로 공유되지 않으며 스레드 안전을 초래하지 않기 때문입니다. 문제.
실제로 모든 동시성 모드는 스레드 안전 문제를 해결할 때 중요한 리소스에 대한 직렬화된 액세스를 사용합니다. 즉, 동시에 단 하나의 스레드만이 중요한 리소스에 액세스할 수 있으며 이를 동기식 상호 배타적 액세스라고도 합니다. 즉, 중요한 리소스에 액세스하는 코드 앞에 잠금을 추가하고 다른 스레드가 계속 액세스할 수 있도록 잠금을 해제합니다.
Java에서는 동기식 상호 배타적 액세스를 구현하기 위해 동기화 및 잠금이라는 두 가지 방법이 제공됩니다. 이 기사에서는 주로 동기화 사용에 대해 설명합니다. Lock 사용은 내 다른 블로그 게시물 "Java Concurrency: Lock Framework 세부 설명"에 설명되어 있습니다.
동기화된 키워드의 사용 방법을 이해하기 전에 먼저 상호 배제 액세스 잠금의 목적을 달성할 수 있는 뮤텍스 잠금 개념을 살펴보겠습니다. 간단한 예로, 중요한 리소스에 뮤텍스 잠금이 추가되면 한 스레드가 중요한 리소스에 액세스하면 다른 스레드는 대기만 할 수 있습니다.
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){} }
从反编译获得的字节码可以看出,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 *///:~
由于字符串常量池的原因,在大多数情况下,同步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 중국어 웹사이트의 기타 관련 기사를 참조하세요!