我刚开始在JAVA学习“多线程”,它接缝我不明白关键字“synchronized”是如何工作的。 我有四个例子,它们非常相似(在我看来),我不明白为什么每次都会得到不同的结果。
public class BufferThread1 {
static int counter = 0;
static StringBuffer s = new StringBuffer();
public static void main(String args[]) throws InterruptedException {
new Thread() {
public void run() {
synchronized (s) {
while (BufferThread1.counter++ < 3) {
s.append("A");
System.out.print("> " + counter + " ");
System.out.println(s);
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Logger.getLogger(BufferThread1.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}.start();
Thread.sleep(100);
while (BufferThread1.counter++ < 6) {
System.out.print("< " + counter + " ");
s.append("B");
System.out.println(s);
}
}
}
Result:
run:
> 1 A
< 2 > 3 AA
AAB
< 5 AABB
< 6 AABBB
public class BufferThread2 {
static int counter = 0;
static StringBuilder s = new StringBuilder();
public static void main(String args[]) throws InterruptedException {
new Thread() {
public void run() {
synchronized (s) {
while (BufferThread2.counter++ < 3) {
s.append("A");
System.out.print("> " + counter + " ");
System.out.println(s);
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Logger.getLogger(BufferThread2.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}.start();
Thread.sleep(100);
while (BufferThread2.counter++ < 6) {
System.out.print("< " + counter + " ");
s.append("B");
System.out.println(s);
}
}
}
Result:
run:
> 1 A
< 2 AB
< 3 ABB
< 4 ABBB
< 5 ABBBB
< 6 ABBBBB
public class BufferThread3 {
static int counter = 0;
static StringBuffer s = new StringBuffer();
public static void main(String args[]) throws InterruptedException {
new Thread() {
public void run() {
synchronized (s) {
while (BufferThread3.counter++ < 3) {
s.append("A");
System.out.print("> " + counter + " ");
System.out.println(s);
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Logger.getLogger(BufferThread3.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}.start();
Thread.sleep(100);
synchronized (s) {
while (BufferThread3.counter++ < 6) {
System.out.print("< " + counter + " ");
s.append("B");
System.out.println(s);
}
}}
}
Result:
run:
> 1 A
> 2 AA
> 3 AAA
< 5 AAAB
< 6 AAABB
当然,我跳过了 import java.util.logging.Level; import java.util.logging.Logger;
我只是没有意识到这些示例如何在这里工作和同步! 我希望有人帮助我。
答案 0 :(得分:1)
您的<
和>
标签在哪个帖子运行。
> 1 A
您的后台线程仅在运行,并按预期打印此行。
< 2
主线程打印计数器2
但无法获取s
上的锁定,因此它会阻止。
> 3 AA
后台线程再次递增计数器并打印3
和第二个A.在下一次迭代中,它退出并counter == 4
当线程退出时,它会释放锁。
AAB
主线程现在可以获取锁定并附加(&#34; B&#34;)
< 5 AABB
主线程将计数器递增到5并添加另一个B
StringBuffer
是我的宠物仇恨,十年前被StringBuidler取代了。使用它实现一个有用的线程安全类几乎是不可能的,当人们尝试时,它意味着该类并非真正的线程安全。注意:SimpleDateFormat使用StringBuffer,它不是线程安全的。
答案 1 :(得分:1)
让我们尝试一个更简单的例子,它可以得到synchronized
没有这么过于复杂的类的内容。
考虑以下Runnable
任务:
public class Task implements Runnable {
private final int id;
public Task(int id) {
this.id = id;
}
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println("Task " + id + " prints " + i);
}
}
}
然后让我们尝试使用这个主要方法运行它:
public static void main(String[] args) {
Thread t1 = new Thread(new Task(1));
Thread t2 = new Thread(new Task(2));
t1.start();
t2.start();
}
输出看起来像这样:
Task 1 prints 0
Task 1 prints 1
Task 1 prints 2
Task 1 prints 3
Task 1 prints 4
Task 2 prints 0
Task 2 prints 1
Task 2 prints 2
Task 2 prints 3
Task 2 prints 4
但它也可能看起来像这样:
Task 1 prints 0
Task 2 prints 0
Task 2 prints 1
Task 1 prints 1
Task 1 prints 2
Task 1 prints 3
Task 1 prints 4
Task 2 prints 2
Task 2 prints 3
Task 2 prints 4
Task 2 prints 4
事实是,您无法保证两个任务执行命令的顺序(相对于彼此。您仍然知道每个任务都将打印0...4
订购)。 t1.start()
在t2.start()
之前出现的事实毫无意义。
synchronized
命令允许对何时执行的线程进行一些控制。实质上,命令synchronized(obj) {....}
意味着最多允许一个线程一次执行块中的命令(在{...}
内)。这被称为互斥。
考虑它的好方法是作为一个上锁的房间,墙上挂着一把钥匙。为了进入房间,你必须把钥匙从墙上取下来,打开门,进入房间,然后从里面锁上。一旦你完成了你在房间里做的任何事情,你就可以从内部解锁门,走到外面,从外面锁上门,然后把钥匙挂起来。很明显,当你在房间里时,没有其他人可以加入你,因为门是锁定的,你现在只能拿着钥匙进入。
为了说明,请考虑以下改进的任务类:
public class SynchronizedTask implements Runnable {
private final Object lock;
private final int id;
public Task(int id, Object lock) {
this.id = id;
this.lock = lock;
}
public void run() {
synchronized(lock) {
for(int i = 0; i < 5; i++) {
System.out.println("Task " + id + " prints " + i);
}
}
}
}
使用以下修改后的main方法运行它:
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Thread(new Task(1, lock));
Thread t2 = new Thread(new Task(2, lock));
t1.start();
t2.start();
}
我们仍然不知道t1
或t2
是否会先进入synchronized
块。但是,一旦进入,另一个必须等待它完成。因此,该代码恰好有两种可能的输出。 t1
首先进入:
Task 1 prints 0
Task 1 prints 1
Task 1 prints 2
Task 1 prints 3
Task 1 prints 4
Task 2 prints 0
Task 2 prints 1
Task 2 prints 2
Task 2 prints 3
Task 2 prints 4
或者t2
先进入:
Task 2 prints 0
Task 2 prints 1
Task 2 prints 2
Task 2 prints 3
Task 2 prints 4
Task 1 prints 0
Task 1 prints 1
Task 1 prints 2
Task 1 prints 3
Task 1 prints 4
请务必注意,此行为只能按预期工作,因为我们对两个任务使用了相同的lock
对象。如果我们改为运行以下代码:
public static void main(String[] args) {
Thread t1 = new Thread(new Task(1, new Object()));
Thread t2 = new Thread(new Task(2, new Object()));
t1.start();
t2.start();
}
我们将与原始的未同步代码具有相同的行为。这是因为我们现在有一个房间有两个(或者实际上是无限的,如果我们复制我们的线程初始化)键挂在外面。因此,虽然t1
位于synchronized
块内,但t2
可以使用自己的密钥进入,从而打败整个目的。
答案 2 :(得分:0)
Lawrey先生所示的第一个例子。
在第二个例子中,“Background”线程只能进行一次打印,因为这次使用StringBuilder而不是StringBuffer,主线程在尝试打印“s”时不会阻塞,因此只有1个。
在第三个示例中,主线程被阻塞,直到后台线程终止,因为您在主线程的循环之前启动后台线程。因此,后台线程将完成3个循环,因此将获得3个A。
虽然我怀疑这些是用于学习目的的人为例子,但仍应注意睡在同步块内并不是一个好主意,因为这不会释放锁定。