服务时间:8:30-18:00

首页 >java学习网

如何理解线程同步

发布时间:2022-11-27 17:16 字数:3972字 阅读:96

如何理解线程同步

程序代码如下:

        public class ThreadPrint {

              static Thread makeThread(final String id, boolean daemon) {
                  Thread t = new Thread(id) {
                      public void run() {
                            System.out.println(id);
                      }
                  };
                  t.setDaemon(daemon);
                  t.start();
                  return t;
              }

              public static void main(String[] args) {
                  Thread a = makeThread("A", false);
                  Thread b = makeThread("B", true);
                  System.out.println("END\n");
              }

        }

请选择正确的答案:

(a)总是打印字符A。

(b)总是打印字符B。

(c)从不在END之后打印A。

(d)从不在END之后打印B。

(e)程序可能依次打印B、End和A。

考点:考察求职者对线程同步的理解和认识。

出现频率:★★★★

【面试题解析】

线程共享了相同的资源。但是在某些重要的情况下,一次只能让一个线程来访问共享资源,例如,作为银行账户这样的共享资源,如果多个线程可以同时使用该资源,就会出现银行账户数据安全上的问题。

1.共享变量

要使多个线程在一个程序中有用,必须有某种方法实现线程间互相通信或共享结果,最简单的方法是使用共享变量。使用同步来确保值从一个线程正确传播到另一个线程,以及防止当一个线程正在更新一些相关数据项时,另一个线程看到不一致的中间结果。

2.存在于同一个内存空间中的所有线程

线程与进程有许多共同点,不同的是线程与同一进程中的其他线程共享相同的进程上下文,包括内存空间。只要访问共享变量(静态或实例变量),线程就可以方便地互相交换数据,但必须确保线程以受控的方式访问共享变量,以免它们互相干扰对方的更改。

3.受控访问的同步

为了确保可以在线程之间以受控方式共享数据,Java语言提供了两个关键字:synchronized和volatile。

Synchronized有以下两个重要含义。

• 一次只有一个线程可以执行代码的受保护部分。

• 一个线程更改的数据对于其他线程是可见的。

如果没有同步,数据很容易就处于不一致状态。例如,如果一个线程正在更新两个相关值,而另一个线程正在读取这两个值,有可能在第1 个线程只写了一个值,还没有写另一个值的时候,调度第2个线程运行,这样它就会看到一个旧值和一个新值。

4.确保共享数据更改的可见性

同步可以让用户确保线程看到一致的内存视图。

处理器可以使用高速缓存加速对内存的访问(或者编译器可以将值存储到寄存器中,以便进行更快的访问)。这表示在这样的系统上,对于同一变量,在两个不同处理器上执行的两个线程可能会看到两个不同的值。如果没有正确的同步,线程可能会看到旧的变量值,或者引起其他形式的数据损坏。

volatile比同步更简单,只适合于控制对基本变量(整数、布尔变量等)的单个实例的访问。当一个变量被声明成volatile,任何对该变量的写操作都会绕过高速缓存,直接写入主内存,而任何对该变量的读取也都绕过高速缓存,直接取自主内存。这表示所有线程在任何时候看到的volatile变量值都相同。

5.用锁保护的原子代码块

Volatile对于确保每个线程看到最新的变量值非常有用,但实际上经常需要保护代码片段,同步使用监控器(monitor)或锁的概念,以协调对特定代码块的访问。

每个Java对象都有一个相关的锁,同一时间只能有一个线程持有Java锁。当线程进入synchronized代码块时,线程会阻塞并等待,直到锁可用。当线程处于就绪状态时,并且获得锁后,将执行代码块,当控制退出受保护的代码块,即到达了代码块末尾或者抛出了没有在synchronized块中捕获的异常时,它就会释放该锁。

这样,每次只有一个线程可以执行受给定监控器保护的代码块。从其他线程的角度看,该代码块可以看作是原子的,它要么全部执行,要么根本不执行。

6.简单的同步示例

使用synchronized块可以将一组相关更新作为一个集合来执行,而不必担心其他线程中断或得到意外的结果。以下示例代码将打印“1 0”或“0 1”。如果没有同步,它还会打印“1 1”或“0 0”。

        public class SyncExample {
        private static lockObject = new Object();
        private static class Thread1 extends Thread {
        public void run() {
        synchronized (lockObject) {
        x = y = 0;
        System.out.println(x);
        }
        }
        }

        private static class Thread2 extends Thread {
        public void run() {
        synchronized (lockObject) {
        x = y = 1;
        System.out.println(y);
        }
        }
        }

        public static void main(String[] args) {
        new Thread1().run();
        new Thread2().run();
        }
        }

在这两个线程中都必须使用同步,以便使程序正确工作。

7.Java锁定

Java锁定可以保护许多代码块或方法,每次只有一个线程可以持有锁。

反之,仅仅因为代码块由锁保护并不表示两个线程不能同时执行该代码块。它只表示如果两个线程正在等待相同的锁,则它们不能同时执行该代码。

在以下示例中,两个线程可以同时不受限制地执行setLastAccess()方法中的synchronized块,因为每个线程有一个不同的thingie值。因此,synchronized代码块受到两个正在执行的线程中不同锁的保护。

        public class SyncExample {
        public static class Thingie {
        private Date lastAccess;

        public synchronized void setLastAccess(Date date) {
        this.lastAccess = date;
        }
        }

        public static class MyThread extends Thread {
        private Thingie thingie;

        public MyThread(Thingie thingie) {
        this.thingie = thingie;
        }

        public void run() {
        thingie.setLastAccess(new Date());
        }
        }

        public static void main() {
        Thingie thingie1 = new Thingie(),
        thingie2 = new Thingie();

        new MyThread(thingie1).start();
        new MyThread(thingie2).start();
        }
        }

8.同步的方法

创建synchronized块的最简单方法是将方法声明成synchronized。这表示在进入方法主体之前,调用者必须获得锁。示例代码如下:

        public class Point {
        public synchronized void setXY(int x, int y) {
        this.x = x;
        this.y = y;
        }
        }

对于普通的synchronized方法,这个锁是一个对象,将针对它调用方法。对于静态synchronized方法,这个锁是与Class对象相关的监控器,在该对象中声明了方法。

setXY()方法被声明成synchronized,并不表示两个不同的线程不能同时执行setXY()方法,只要它们调用不同的Point实例的setXY()方法,就可同时执行。对于一个Point实例,一次只能有一个线程执行setXY()方法,或Point的任何其他synchronized方法。

9.同步的块

synchronized块的语法比synchronized方法稍微复杂一点,因为还需要显式地指定锁要保护哪个块。

        public class Point {
        public void setXY(int x, int y) {
        synchronized (this) {
        this.x = x;
        this.y = y;
        }
        }
        }

使用this引用作为锁很常见,但这并不是必需的。使用this引用作为锁表示该代码块将与这个类中的synchronized方法使用同一个锁。

由于同步防止了多个线程同时执行一个代码块,因此性能上就有问题,即使是在单处理器系统上,也最好在尽可能小的需要保护的代码块上使用同步。

访问基于堆栈的局部变量从来不需要受到保护,因为它们只能被自己所属的线程访问。

10.大多数类并没有同步

因为同步会带来小小的性能损失,大多数通用类,如java.util中的Collection类,不在内部使用同步。这表示在没有附加同步的情况下,不能在多个线程中使用诸如HashMap这样的类。

面试题中,因为调度程序的确切任务是未定的,所以打印文本的次序也是任意的。打印B的线程是一个后台线程,意味着程序可以在线程设法打印字母之前终止。

参考答案:(a)、(e)。