我无法理解同步工作的例子

时间:2016-01-17 20:41:21

标签: java multithreading

我刚开始在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;

我只是没有意识到这些示例如何在这里工作和同步! 我希望有人帮助我。

3 个答案:

答案 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();
}

我们仍然不知道t1t2是否会先进入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。

虽然我怀疑这些是用于学习目的的人为例子,但仍应注意睡在同步块内并不是一个好主意,因为这不会释放锁定。