java多线程

互斥同步

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock

线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果

同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能
知识兔

synchronized

  1. 修饰代码块

    public void func() {
        synchronized (this) {
            // ...
        }
    }
    知识兔

    它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步

  2. 修饰一个方法

    public synchronized void func () {
        // ...
    }
    知识兔

    它和同步代码块一样,作用于同一个对象

  3. 同步一个类

    public class SynchronizedExample {
        public void func2() {
            synchronized (SynchronizedExample.cl ass) {
                for (int i = 0; i < 10; i++) {
                    System.out.print(i + " ");
                }
            }
        }
    }
    知识兔

    作用的对象是这个类的所有对象

  4. 同步一个静态方法

    public synchronized static void fun() {
        // ...
    }
    知识兔

    静态的同步函数的锁是:字节码对象,用于整个类

ReentrantLock

重入锁(ReentrantLock)是一种递归无阻塞的同步机制。它是 java.util.concurrent(J.U.C)包中的锁

public class LockExample {
    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 确保释放锁,从而避免发生死锁。
        }
    }
}
知识兔
public static void main(String[] args) {    LockExample lockExample = new LockExample();    ExecutorService executorService = Executors.newCachedThreadPool();    executorService.execute(() -> lockExample.func());    executorService.execute(() -> lockExample.func());}// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

相比于 synchronized,它多了以下高级功能:

  1. 等待可中断

    当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

  2. 可实现公平锁

    公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

  3. 锁绑定多个条件

    一个 ReentrantLock 对象可以同时绑定多个 Condition 对象。

synchronized 和 ReentrantLock 比较

1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。

3. 功能

ReentrantLock 多了一些高级功能。

4. 使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

常见问题:synchronized与lock的区别,使用场景。看过synchronized的源码没?

  • (用法)synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized 可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
  • (用法)lock(显示锁):需要显示指定起始位置和终止位置。一般使用 ReentrantLock 类做为锁,多个线程中必须要使用一个 ReentrantLock 类做为对象才能保证锁的生效。且在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。
  • (性能)synchronized 是托管给 JVM 执行的,而 lock 是 Java 写的控制锁的代码。在 Java1.5 中,synchronize 是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用 Java 提供的 Lock 对象,性能更高一些。但是到了 Java1.6 ,发生了变化。synchronize 在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致 在 Java1.6 上 synchronize 的性能并不比 Lock 差。
  • (机制)synchronized 原始采用的是 CPU 悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。Lock 用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是 CAS 操作(Compare and Swap)。

什么是乐观锁和悲观锁

  • 为什么需要锁(并发控制)
    • 在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。
    • 典型的冲突有:
      • 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户 A 把值从 6 改为 2,用户 B 把值从 2 改为 6,则用户 A 丢失了他的更新。
      • 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户 A,B 看到的值都是6,用户 B 把值改为 2,用户 A 读到的值仍为 6。
    • 为了解决这些并发带来的问题。 我们需要引入并发控制机制。
  • 并发控制机制
    • 悲观锁:假定会发生并发冲突,独占锁,屏蔽一切可能违反数据完整性的操作。
    • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁不能解决脏读的问题。

参考资料:

Synchronized(对象锁)和Static Synchronized(类锁)区别

  • 一个是实例锁(锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念),一个是全局锁(该锁针对的是类,无论实例多少个对象,那么线程都共享该锁)。

    实例锁对应的就是 synchronized关 键字,而类锁(全局锁)对应的就是 static synchronized(或者是锁在该类的 class 或者 classloader 对象上)。

/** * static synchronized 和synchronized的区别! * 关键是区别第四种情况! */public class StaticSynchronized {    /**     * synchronized方法     */    public synchronized void isSynA(){        System.out.println("isSynA");    }    public synchronized void isSynB(){        System.out.println("isSynB");    }    /**     * static synchronized方法     */    public static synchronized void cSynA(){        System.out.println("cSynA");    }    public static synchronized void cSynB(){        System.out.println("cSynB");    }    public static void main(String[] args) {        StaticSynchronized x = new StaticSynchronized();        StaticSynchronized y = new StaticSynchronized();        /**         *  x.isSynA()与x.isSynB(); 不能同时访问(同一个对象访问synchronized方法)         *  x.isSynA()与y.isSynB(); 能同时访问(不同对象访问synchronized方法)         *  x.cSynA()与y.cSynB(); 不能同时访问(不同对象也不能访问static synchronized方法)         *  x.isSynA()与y.cSynA(); 能同时访问(static synchronized方法占用的是类锁,         *                        而访问synchronized方法占用的是对象锁,不存在互斥现象)         */    }}

线程之间的协作

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调

  • join()
    在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

    /*对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。*/public class JoinExample {    private class A extends Thread {        @Override        public void run() {            System.out.println("A");        }    }    private class B extends Thread {        private A a;        B(A a) {            this.a = a;        }        @Override        public void run() {            try {                a.join();            } catch (InterruptedException e) {                e.printStackTrace();            }            System.out.println("B");        }    }    public void test() {        A a = new A();        B b = new B(a);        b.start();        a.start();    }}public static void main(String[] args) {    JoinExample example = new JoinExample();    example.test();}//输出: A B
  • wait() notify() notifyAll()
    调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify()(随机叫醒一个) 或者 notifyAll() (叫醒所有 wait 线程,争夺时间片的线程只有一个)来唤醒挂起的线程。

    它们都属于 Object 的一部分,而不属于 Thread。
    只能用在同步方法或者同步控制块中使用!否则会在运行时抛出 IllegalMonitorStateExeception。

    使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

  • await() signal() signalAll()
    java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

    使用 Lock 来获取一个 Condition 对象。

  • sleep和wait有什么区别

    • sleep 和 wait
      • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
      • wait() 会释放锁,sleep() 不会。
    • 有什么区别
      • sleep() 方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)。
      • wait() 是 Object 类的方法,调用对象的 wait() 方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 notify() 方法(或 notifyAll() 方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
计算机