我对 synchronized
关键字的用法和意义有一些疑问。
synchronized 关键字的意义是什么?
什么时候应该同步方法?
它在程序上和逻辑上意味着什么?
synchronized
关键字是关于读取和写入相同变量、对象和资源的不同线程。这在 Java 中不是一个微不足道的话题,但这里引用了 Sun 的一句话:
同步方法启用了一种防止线程干扰和内存一致性错误的简单策略:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都通过同步方法完成。
简而言之:当您有两个线程正在读取和写入同一个“资源”时,比如说一个名为 foo
的变量,您需要确保这些线程访问该变量以原子方式。如果没有 synchronized
关键字,您的线程 1 可能看不到线程 2 对 foo
所做的更改,或者更糟的是,它可能只更改了一半。这不是您在逻辑上所期望的。
同样,这在 Java 中是一个重要的话题。要了解更多信息,请在此处探索关于 SO 和 Interwebs 的主题:
并发
Java 内存模型
继续探索这些主题,直到“Brian Goetz”这个名字在你的大脑中与“并发”一词永久相关。
好吧,我想我们已经有足够的理论解释了,所以考虑一下这段代码
public class SOP {
public static void print(String s) {
System.out.println(s+"\n");
}
}
public class TestThread extends Thread {
String name;
TheDemo theDemo;
public TestThread(String name,TheDemo theDemo) {
this.theDemo = theDemo;
this.name = name;
start();
}
@Override
public void run() {
theDemo.test(name);
}
}
public class TheDemo {
public synchronized void test(String name) {
for(int i=0;i<10;i++) {
SOP.print(name + " :: "+i);
try{
Thread.sleep(500);
} catch (Exception e) {
SOP.print(e.getMessage());
}
}
}
public static void main(String[] args) {
TheDemo theDemo = new TheDemo();
new TestThread("THREAD 1",theDemo);
new TestThread("THREAD 2",theDemo);
new TestThread("THREAD 3",theDemo);
}
}
注意:只要前一个线程的执行未完成,synchronized
就会阻止下一个线程对方法 test() 的调用。线程一次可以访问这个方法。如果没有 synchronized
,所有线程都可以同时访问此方法。
当一个线程调用对象的同步方法'test'(这里对象是'TheDemo'类的一个实例)它获取该对象的锁,任何新线程都不能调用同一个对象的任何同步方法,只要前一个线程获得锁的不会释放锁。
当调用类的任何静态同步方法时,也会发生类似的事情。线程获取与类关联的锁(在这种情况下,任何线程都可以调用该类实例的任何非静态同步方法,因为该对象级锁仍然可用)。只要当前持有锁的线程没有释放类级别的锁,任何其他线程将无法调用该类的任何静态同步方法。
同步输出
THREAD 1 :: 0
THREAD 1 :: 1
THREAD 1 :: 2
THREAD 1 :: 3
THREAD 1 :: 4
THREAD 1 :: 5
THREAD 1 :: 6
THREAD 1 :: 7
THREAD 1 :: 8
THREAD 1 :: 9
THREAD 3 :: 0
THREAD 3 :: 1
THREAD 3 :: 2
THREAD 3 :: 3
THREAD 3 :: 4
THREAD 3 :: 5
THREAD 3 :: 6
THREAD 3 :: 7
THREAD 3 :: 8
THREAD 3 :: 9
THREAD 2 :: 0
THREAD 2 :: 1
THREAD 2 :: 2
THREAD 2 :: 3
THREAD 2 :: 4
THREAD 2 :: 5
THREAD 2 :: 6
THREAD 2 :: 7
THREAD 2 :: 8
THREAD 2 :: 9
输出不同步
THREAD 1 :: 0
THREAD 2 :: 0
THREAD 3 :: 0
THREAD 1 :: 1
THREAD 2 :: 1
THREAD 3 :: 1
THREAD 1 :: 2
THREAD 2 :: 2
THREAD 3 :: 2
THREAD 1 :: 3
THREAD 2 :: 3
THREAD 3 :: 3
THREAD 1 :: 4
THREAD 2 :: 4
THREAD 3 :: 4
THREAD 1 :: 5
THREAD 2 :: 5
THREAD 3 :: 5
THREAD 1 :: 6
THREAD 2 :: 6
THREAD 3 :: 6
THREAD 1 :: 7
THREAD 2 :: 7
THREAD 3 :: 7
THREAD 1 :: 8
THREAD 2 :: 8
THREAD 3 :: 8
THREAD 1 :: 9
THREAD 2 :: 9
THREAD 3 :: 9
synchronized
的锁定行为,但忽略了内存一致性。
SOP
并重构代码。
synchronized
关键字可防止多个线程同时访问代码块或对象。 Hashtable
的所有方法都是 synchronized
,因此一次只有一个线程可以执行其中任何一个。
使用 HashMap
等非 synchronized
构造时,您必须在代码中构建线程安全功能以防止出现一致性错误。
synchronized
表示在多线程环境中,具有 synchronized
方法/块的对象不会让两个线程访问 synchronized
方法/块的代码同时。这意味着一个线程无法读取,而另一个线程更新它。
第二个线程将改为等待第一个线程完成其执行。开销是速度,但优点是保证数据的一致性。
但是,如果您的应用程序是单线程的,则 synchronized
块不会提供任何好处。
synchronized
关键字使线程在进入方法时获得锁,这样只有一个线程可以同时执行该方法(对于给定的对象实例,除非它是静态方法)。
这通常被称为使类线程安全,但我会说这是一种委婉说法。虽然同步确实可以保护 Vector 的内部状态不被破坏,但这通常对 Vector 的用户没有多大帮助。
考虑一下:
if (vector.isEmpty()){
vector.add(data);
}
即使所涉及的方法是同步的,因为它们是单独锁定和解锁的,两个不幸的定时线程可以创建一个包含两个元素的向量。
因此,实际上,您还必须在应用程序代码中进行同步。
因为方法级同步 a) 在不需要时很昂贵,并且 b) 在需要同步时不够,所以现在有非同步的替换(在 Vector 的情况下为 ArrayList)。
最近,并发包已经发布,其中包含许多处理多线程问题的巧妙实用程序。
概述
Java中的同步关键字与线程安全有关,即当多个线程读取或写入同一个变量时。这可以直接(通过访问相同的变量)或间接(通过使用使用另一个访问相同变量的类的类)发生。
synchronized 关键字用于定义一个代码块,其中多个线程可以安全地访问同一个变量。
更深层次的
在语法方面,synchronized
关键字将 Object
作为其参数(称为锁定对象),然后是 { block of code }
。
当执行遇到此关键字时,当前线程会尝试“锁定/获取/拥有”(选择)锁定对象,并在获得锁定后执行相关的代码块。
保证对同步代码块内的变量的任何写入对于使用相同锁对象类似地执行同步代码块内的代码的每个其他线程都是可见的。
一次只有一个线程可以持有锁,在此期间所有其他试图获取相同锁对象的线程将等待(暂停它们的执行)。当执行退出同步代码块时,锁将被释放。
同步方法:
将 synchronized
关键字添加到方法定义中等于将整个方法主体包装在同步代码块中,其中 lock 对象 为 this
(例如方法)和 ClassInQuestion.getClass()
(用于类方法)。
- 实例方法是没有 static
关键字的方法。
- 类方法是有 static
关键字的方法。
技术的
如果没有同步,则无法保证读取和写入的顺序,可能会给变量留下垃圾。 (例如,一个变量可能会以一个线程写入的一半位和另一个线程写入的一半位结束,从而使变量处于两个线程都没有尝试写入的状态,而是两者的混合状态。)
在另一个线程读取它之前(挂钟时间)在一个线程中完成写入操作是不够的,因为硬件可能已经缓存了变量的值,并且读取线程将看到缓存的值而不是写入的内容它。
结论
因此,在 Java 的情况下,您必须遵循 Java 内存模型以确保不会发生线程错误。换句话说:使用同步、原子操作或在后台为您使用它们的类。
来源 http://docs.oracle.com/javase/specs/jls/se8/html/index.html Java® 语言规范,2015-02-13
把它想象成一种旋转栅门,就像你在足球场上发现的那样。想要进入的人是平行的,但在十字转门处,他们是“同步的”。一次只能有一个人通过。所有想要通过的人都会这样做,但他们可能必须等到他们可以通过。
什么是同步关键字?
线程主要通过共享对字段和对象引用字段的访问来进行通信。这种通信形式非常高效,但会导致两种错误:线程干扰和内存一致性错误。防止这些错误所需的工具是同步。
同步块或方法可防止线程干扰并确保数据一致。在任何时间点,只有一个线程可以通过获取锁来访问同步块或方法(临界区)。其他线程将等待释放锁以访问临界区。
方法何时同步?
当您将 synchronized
添加到方法定义或声明时,方法会同步。您还可以在方法中同步特定的代码块。
它在程序上和逻辑上是什么意思?
这意味着只有一个线程可以通过获取锁来访问临界区。除非该线程释放该锁,否则所有其他线程将不得不等待获取锁。他们没有获得锁就无法进入临界区。
这是魔法无法做到的。识别应用程序中的关键部分并相应地保护它是程序员的责任。 Java 提供了一个框架来保护你的应用程序,但是所有部分要保护的位置和内容是程序员的责任。
来自 java 文档 page 的更多详细信息
内在锁和同步:
同步是围绕称为内在锁或监视器锁的内部实体构建的。内在锁在同步的两个方面都发挥了作用:强制对对象状态的独占访问和建立对可见性至关重要的先发生关系。
每个对象都有一个与之关联的内在锁。按照惯例,需要对对象字段进行排他和一致访问的线程必须在访问对象之前获取对象的内在锁,然后在完成访问时释放内在锁。
在获得锁和释放锁之间,线程被称为拥有内在锁。只要一个线程拥有一个内在锁,其他线程就不能获得相同的锁。另一个线程在尝试获取锁时会阻塞。
当线程释放内在锁时,会在该操作和任何后续获取相同锁之间建立起之前的关系。
使方法同步有两个effects:
首先,同一对象上的同步方法的两次调用不可能交错。
当一个线程正在为一个对象执行同步方法时,所有其他为同一对象调用同步方法的线程都会阻塞(暂停执行),直到第一个线程处理完该对象。
其次,当同步方法退出时,它会自动与任何后续对同一对象的同步方法调用建立起之前的关系。
这保证了对象状态的更改对所有线程都是可见的。
在以下位置寻找同步的其他替代方法:
Avoid synchronized(this) in Java?
Synchronized normal method
等同于 Synchronized statement
(使用它)
class A {
public synchronized void methodA() {
// all function code
}
equivalent to
public void methodA() {
synchronized(this) {
// all function code
}
}
}
Synchronized static method
等同于 Synchronized statement
(使用类)
class A {
public static synchronized void methodA() {
// all function code
}
equivalent to
public void methodA() {
synchronized(A.class) {
// all function code
}
}
}
同步语句(使用变量)
class A {
private Object lock1 = new Object();
public void methodA() {
synchronized(lock1 ) {
// all function code
}
}
}
对于 synchronized
,我们同时拥有 Synchronized Methods
和 Synchronized Statements
。但是,Synchronized Methods
与 Synchronized Statements
类似,所以我们只需要了解 Synchronized Statements
。
=> 基本上,我们会有
synchronized(object or class) { // object/class use to provides the intrinsic lock
// code
}
这里有 2 个认为有助于理解 synchronized
每个对象/类都有一个与之关联的内在锁。
当线程调用同步语句时,它会自动获取该同步语句对象的内在锁,并在方法返回时释放它。只要一个线程拥有一个内在锁,没有其他线程可以获得相同的锁 => 线程安全。
=>当 thread A
调用 synchronized(this){// code 1}
=>由于 SAME 锁定,所有具有 synchronized(this)
的块代码(类内)和所有 synchronized normal method
(类内)都被锁定。它将在 thread A
解锁(“// 代码 1”完成)后执行。
此行为类似于 synchronized(a variable){// code 1}
或 synchronized(class)
。
SAME LOCK => lock(不依赖于哪种方法?或哪些语句?)
使用同步方法或同步语句?
我更喜欢 synchronized statements
,因为它更具可扩展性。例如,以后只需要同步一部分方法。例如,您有 2 个同步方法,并且它们没有任何彼此相关,但是当一个线程运行一个方法时,它会阻塞另一个方法(它可以通过使用 synchronized(a variable)
来阻止) .
但是,应用同步方法很简单,代码看起来也很简单。对于某些类,只有一个同步方法,或者类中的所有同步方法彼此相关=>我们可以使用 synchronized method
使代码更短且易于理解
笔记
(它与synchronized
无关,它是对象和类或非静态和静态之间的区别)。
当您使用同步或普通方法或同步(此)或同步(非静态变量)时,它将基于每个对象实例进行同步。
当您使用同步或静态方法或同步(类)或同步(静态变量)时,它将基于类同步
参考
https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html
希望有帮助
这是来自 The Java Tutorials 的解释。
考虑以下代码:
公共类 SynchronizedCounter { private int c = 0;公共同步无效增量(){ C++; } 公共同步无效减量() { c--; } 公共同步 int value() { return c;如果 count 是 SynchronizedCounter 的一个实例,那么使这些方法同步有两个效果:首先,对同一个对象的两个同步方法的调用不可能交错。当一个线程正在为一个对象执行同步方法时,所有其他为同一对象调用同步方法的线程都会阻塞(暂停执行),直到第一个线程处理完该对象。其次,当同步方法退出时,它会自动与任何后续对同一对象的同步方法调用建立起之前的关系。这保证了对象状态的更改对所有线程都是可见的。
据我了解,同步基本上意味着编译器围绕您的方法编写了一个 monitor.enter 和 monitor.exit 。因此,它可能是线程安全的,具体取决于它的使用方式(我的意思是,您可以使用非线程安全的同步方法编写一个对象,具体取决于您的类的功能)。
其他答案缺少的是一个重要方面:记忆障碍。线程同步基本上由两部分组成:序列化和可见性。我建议大家在谷歌上搜索“jvm 内存屏障”,因为它是一个重要且极其重要的主题(如果您修改了由多个线程访问的共享数据)。完成之后,我建议查看 java.util.concurrent 包的类,这些类有助于避免使用显式同步,这反过来又有助于保持程序简单高效,甚至可以防止死锁。
ConcurrentLinkedDeque 就是一个这样的例子。与 command pattern 一起,它允许通过将命令填充到并发队列中来创建高效的工作线程 - 不需要显式同步,不会出现死锁,不需要显式 sleep(),只需通过调用 take() 来轮询队列。
简而言之:“内存同步”在您启动线程、线程结束、读取 volatile 变量、解锁监视器(离开同步块/函数)等时隐式发生。这种“同步”影响(在某种意义上“刷新”)所有在该特定操作之前完成的写入。在上述 ConcurrentLinkedDeque 的情况下,文档“说”:
内存一致性效果:与其他并发集合一样,线程中的操作在将对象放入 ConcurrentLinkedDeque 之前发生在另一个线程中从 ConcurrentLinkedDeque 访问或删除该元素之后的操作。
这种隐含的行为是一个有点有害的方面,因为大多数没有太多经验的 Java 程序员会因此而接受很多。然后在 Java 没有按照“应该”在生产环境中执行不同工作负载的情况下突然发现这个线程——而且很难测试并发问题。
同步只是意味着如果在特定对象上使用同步块,则如果与单个对象相关联的多个线程可以防止脏读和写。为了让您更清楚,让我们举个例子:
class MyRunnable implements Runnable {
int var = 10;
@Override
public void run() {
call();
}
public void call() {
synchronized (this) {
for (int i = 0; i < 4; i++) {
var++;
System.out.println("Current Thread " + Thread.currentThread().getName() + " var value "+var);
}
}
}
}
public class MutlipleThreadsRunnable {
public static void main(String[] args) {
MyRunnable runnable1 = new MyRunnable();
MyRunnable runnable2 = new MyRunnable();
Thread t1 = new Thread(runnable1);
t1.setName("Thread -1");
Thread t2 = new Thread(runnable2);
t2.setName("Thread -2");
Thread t3 = new Thread(runnable1);
t3.setName("Thread -3");
t1.start();
t2.start();
t3.start();
}
}
我们创建了两个 MyRunnable 类对象,runnable1 与线程 1 共享,线程 3 & runnable2 仅与线程 2 共享。现在,当 t1 和 t3 在没有使用同步的情况下启动时,PFB 输出表明线程 1 和 3 同时影响 var 值,其中线程 2 的 var 有自己的内存。
Without Synchronized keyword
Current Thread Thread -1 var value 11
Current Thread Thread -2 var value 11
Current Thread Thread -2 var value 12
Current Thread Thread -2 var value 13
Current Thread Thread -2 var value 14
Current Thread Thread -1 var value 12
Current Thread Thread -3 var value 13
Current Thread Thread -3 var value 15
Current Thread Thread -1 var value 14
Current Thread Thread -1 var value 17
Current Thread Thread -3 var value 16
Current Thread Thread -3 var value 18
使用 Synchronzied,线程 3 在所有场景中都在等待线程 1 完成。获得了两个锁,一个在 runnable1 上由线程 1 和线程 3 共享,另一个在 runnable2 上仅由线程 2 共享。
Current Thread Thread -1 var value 11
Current Thread Thread -2 var value 11
Current Thread Thread -1 var value 12
Current Thread Thread -2 var value 12
Current Thread Thread -1 var value 13
Current Thread Thread -2 var value 13
Current Thread Thread -1 var value 14
Current Thread Thread -2 var value 14
Current Thread Thread -3 var value 15
Current Thread Thread -3 var value 16
Current Thread Thread -3 var value 17
Current Thread Thread -3 var value 18
在 java 中,为了防止多个线程操作共享变量,我们使用 synchronized
关键字。让我们借助以下示例来理解它:
在示例中,我定义了两个线程并将它们命名为递增和递减。增量线程将共享变量 (counter
) 的值增加与减量线程减少它的量相同的量,即增加 5000 倍(导致 5000 + 0 = 5000)和我们减少 5000 倍(导致 5000 - 5000 = 0)。
没有 synchronized
关键字的程序:
class SynchronizationDemo {
public static void main(String[] args){
Buffer buffer = new Buffer();
MyThread incThread = new MyThread(buffer, "increment");
MyThread decThread = new MyThread(buffer, "decrement");
incThread.start();
decThread.start();
try {
incThread.join();
decThread.join();
}catch(InterruptedException e){ }
System.out.println("Final counter: "+buffer.getCounter());
}
}
class Buffer {
private int counter = 0;
public void inc() { counter++; }
public void dec() { counter--; }
public int getCounter() { return counter; }
}
class MyThread extends Thread {
private String name;
private Buffer buffer;
public MyThread (Buffer aBuffer, String aName) {
buffer = aBuffer;
name = aName;
}
public void run(){
for (int i = 0; i <= 5000; i++){
if (name.equals("increment"))
buffer.inc();
else
buffer.dec();
}
}
}
如果我们运行上面的程序,我们期望缓冲区的值是相同的,因为将缓冲区增加和减少相同的量会导致我们以正确的开始值?让我们看看输出:
https://i.stack.imgur.com/upqWt.jpg
正如您所看到的,无论我们运行程序多少次,我们都会得到不同的结果,原因是每个线程同时操作 counter
。如果我们可以设法让一个线程首先增加共享变量,然后再减少它,反之亦然,我们将获得正确的结果,这正是通过添加 synchronized
关键字可以完成的 synchronized
关键字在 Buffer
的 inc
和 dec
方法之前,如下所示:
使用 synchronized
关键字的程序:
// rest of the code
class Buffer {
private int counter = 0;
// added synchronized keyword to let only one thread
// be it inc or dec thread to manipulate data at a time
public synchronized void inc() { counter++; }
public synchronized void dec() { counter--; }
public int getCounter() { return counter; }
}
// rest of the code
和输出:
https://i.stack.imgur.com/o4CGa.jpg
无论我们运行多少次,我们都会得到与 0 相同的输出
同步简单意味着没有两个线程可以同时访问块/方法。当我们说类的任何块/方法是同步的时,这意味着一次只有一个线程可以访问它们。在内部,尝试访问它的线程首先会对该对象进行锁定,并且只要该锁定不可用,就没有其他线程可以访问该类实例的任何同步方法/块。
请注意,另一个线程可以访问未定义为同步的同一对象的方法。线程可以通过调用释放锁
Object.wait()
synchronized 是 Java 中的一个关键字,用于在多线程环境中使关系发生在关系之前,以避免内存不一致和线程干扰错误。