Day05
1. 说说wait()和sleep()的区别
- 所属的类型不同:wait()是Object类的实例方法,调用该方法的线程将进入waiting状态;sleep()是Thread类的静态方法,调用该方法的线程将进入Timed_waiting状态
- 对锁的依赖不同:wait()依赖于synchronized锁,他必须通过监视器就进行调用,在调用后线程会释放锁;sleep()不依赖于任何锁,所以在调用后他也不会释放锁
- 返回的条件不同:调用wait()进入等待状态的线程,需要由notify()/notifyAll()唤醒,从而返回;调用sleep()进入超时等待的线程,需要在超时时间到达后自动返回
- wait()方法也支持超时参数,线程调用带有超时参数的wait()会进入Timed_waiting状态,在此状态下的线程可以通过notify/notifyAll唤醒,从而返回,若在达到超时时间后仍然未被唤醒则自动返回
- 如果采用Lock进行线程同步,则不存在同步监视器,此时需要使用Condition的方法实现等待;Condition对象是通过Lock对象创建出来的,它的await()方法会导致线程进入waiting状态,它的带超时参数的await()方法会导致线程进入Timed_waiting状态,当调用它的signal()/signalAll()方法时,线程会被唤醒,从而返回
- sleep没有释放锁,wait释放锁;两者都可以暂停线程的执行;wait用于线程间的通信,sleep用于暂停线程;wait等待过程不占有cpu,sleep仍然占用;wait调用后,需要别的线程调用同一个对象的notify方法
2. 说说怎么保证线程安全
- 线程安全问题是指在多线程背景下,线程没有按照我们的预期执行,导致操作共享变量出现异常
- 在Java中有许多同步方案提供给我们使用,从轻到重有三种方式:原子类、volatile关键字、锁;原子类是JUC atomic包下的一系列类,通过CAS比较与交换机制实现线程安全的更新共享变量,通过预期值与内存值的比较来判断是否修改
- volatile关键字是轻量级的同步机制,它实现了变量的可见性,防止指令的重排序,保证了单个变量的读写线程安全;可见性问题是JVM内存模型中定义没有核心存在一个内存副本导致的,核心只操作他们的内存副本,而volatile保证了一旦修改变量则立即刷新到共享内存中,且其他核心的内存副本失效,需要重新读取
- 原子类和volatile只能保证单个共享变量的线程安全,锁则可以保证临界区内的多个共享变量的线程安全
- Java中常用的锁有两种:synchronized+juc包下的lock锁。synchronized锁是互斥锁,可以作用于实例方法、静态方法、代码块,基于对象头和Monitor对象,在1.6以后引入轻量级锁和偏向锁等优化;
- lock锁接口可以通过lock、unlock方法锁住一段代码,基于AQS实现,其加锁解锁就是操作AQS的state变量,并且将阻塞队列存于AQS的双向队列中
- 除了锁以外,juc包下还提供了一些线程同步工具类,如CountDownLatch、Semaphore等等,我们还可以使用ThreadLoacl定义线程局部变量
3. 说说你了解的线程同步方式
- Java主要通过加锁的方式实现线程同步,而锁有两类,分别是synchronized和lock
- synchronized可以加在三个不同位置,对应三种不同的使用方式,这三种方式的区别是锁对象不同:1.加在普通方法上,则锁是当前的实力(this);2.加在静态方法上,则锁是当前类的Class对象;3.加载代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象
- 不同的锁对象,意味着不同的锁粒度,所以我们不应该无脑地将它加在方法前了事,尽管通常这可以解决问题。而是应该根据要锁定的范围,准确地选择锁对象,从而准确地确定锁粒度,降低锁带来的性能开销
- synchronized是比较早期的API,在设计之初没有考虑到超时机制、非阻塞形式以及多个条件变量。若想通过升级的方式让synchronized支持这些相对复杂的功能,则需要大改它的语法结构,不利于兼容旧代码。
- JDK的开发团队在1.5引入了Lock接口,并通过Lock支持了上述的功能。Lock支持的功能包括:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)。
- synchronized采用“CAS+Mark Word”实现,为了性能的考虑,并通过锁升级机制降低性能的开销。在并发环境中,synchronized会随着多线程竞争的加剧,按照如下步骤逐步升级:无锁、偏向锁、轻量级锁、重量级锁。Lock则采用“CAS+volatile”实现,其实现的核心是AQS。
- AQS是线程同步器,是一个线程同步的基础框架,它基于模版方法模式。在具体的Lock实例中,锁的实现是通过继承AQS来实现的,并且可以根据锁的使用场景,派生出公平锁,不公平锁,读锁,写锁等具体的实现
4. 说说你了解的线程通信方式
- 在Java中,常用的线程通信方式有两种,分别是利用Monitor实现线程通信、利用Condition实现线程通信。
- 线程同步是线程通信的前提,所以究竟采用哪种方式实现通信,取决于线程同步的方式。
- 如果采用synchronized关键字进行同步,则需要依赖Monitor(同步监视器)实现线程通信,Monitor就是锁对象。在synchronized同步模式下,锁对象可以是任意类型,所以通信方法自然就被定义在了Object类中了,这些方法包括:wait()、notify、notifyAll。一个线程通过Monitor调用wait()时,他就会释放锁并在此等待。当其他线程通过Monitor调用notify()时,则会唤醒在此等待的一个线程。当其他线程通过Monitor调用notifyAll时,则会唤醒在此等待的所有线程。
- JDK1.5新增了Lock接口及其实现类,提供了更为灵活的同步方式。如果是采用lock对象进行同步,则需要依赖condition实现线程通信,Condition对象是由Lock对象创建出来的,它依赖于Lock对象。Condition对象中定义的通信方法,与Object类中的通信方法类似,它包括await()、signal()、signalAll()。
- 线程同步是基于同步队列实现的,而线程通信是基于等待队列实现的。当调用等待方法时,即将当前线程加入等待队列。当调用通知方法时,即将等待队列中的一个或多个线程转移回同步队列。因为synchronized只有一个Monitor,所以它就只有一个等待队列。而Lock对象可以创建出多个Condition,所以它拥有多个等待队列。多个等待队列带来了极大的灵活性,所以基于condition的通信方式更为推荐
- 比如,在实现生产消费模型时,生产者要通知消费者、消费者要通知生产者。相反,不应该出现生产者通知生产者、消费者通知消费者这样的情况。如果使用synchronized实现这个模型,由于它只有一个等待队列,所以只能把生产者和消费者加入同一个队列,这就会导致生产者通知生产者、消费者通知消费者的情况出现。采用Lock实现这个模型时,由于它有多个等待队列,可以有效地将这两个角色区分开,就能避免出现这样的问题。
5. 说说Java中常用的锁及原理
- Java中加锁有两种方式,分别是synchronized关键字和Lock接口,而Lock接口的经典实现是ReentrantLock。另外还有ReadWriteLock接口,它的内部设计了两把锁分别用于读写,这两把锁都是lock类型,它的经典实现是ReentrantReadWriteLock。
- 其中,synchronized的实现依赖于对象头,Lock接口的实现则依赖于AQS。
- synchronized的底层是采用Java对象头来存储锁信息的,对象头包含了三个部分,分别是Mark Word、Class Metadta Address、Array length。其中,Mark Word用来存储对象的hashCode及锁信息,Class Metadata Address用来存储对象类型的指针,而Array length则用来存储数组对象的长度。
- AQS是队列同步器,是用来构建锁的基础框架,Lock实现类都是基于AQS实现的。AQS是基于模版方法模式设计的,所以锁的实现需要继承AQS并重写它的指定的方法。AQS内部定义了一个FIFO队列来实现线程的同步,同时还定义了同步状态来记录锁的信息。
- ReentrantLock通过内部类Sync定义了锁,他还定义了Sync的两个子类FrSync和NonfrSync,这两个子类分别代表了公平锁和不公平锁。Sync继承自AQS,它不仅仅使用了AQS的同步状态记录锁的信息,还利用同步状态记录了重入次数。同步状态是一个整数,当它为0时代表无锁,当它为N时代表线程持有了锁并重入了N次