一、 volatile关键字
作用:当多个线程操作共享数据时,可以保证内存中的数据是可见的。相较于synchronized是一种比较轻量级的同步策略。
注意:
1、volatile不具备“互斥性”
2、valatile不能保证变量的“原子性”
问题引入:看下面这段代码
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true){
if (td.isFlag()){
System.out.println("------------------");
break;
}
}
}
}
class ThreadDemo implements Runnable{
private boolean flag = false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag="+flag);
}
}
运行结果:
内存可见性问题是,当多个线程操作共享数据时,彼此不可见
二、原子变量与CAS算法
i++的原子性问题:i++的操作实际上分为三个步骤“读、改、写”
原子变量:jdk1.5后java.util.concurrent.atomic包下提供了常用的原子变量:
1、volatile保证内存可见性
2、CAS(compare and swap)算法保证数据的原子性
CAS算法是硬件对于并发操作共享数据的支持。
CAS包含三个操作数:
内存值V、预估值(旧值)A、更新值B,当前仅当V == A 时,V=B,否则,将不做任何操作。
三、同步容器
ConcurrentHashMap:
采用“锁分段”机制
四、Lock接口
4.1 复习Synchrinized
多线程编程模板上
1、先写资源类
2、再写操作(操作是资源类里面的)
3、再写线程,调用资源类里面的方法
(线程 操作 资源类高内聚低耦合)
4.2 Lock
是什么
Lock接口的实现
ReentrantLock()
创建线程方式
public class SaleTicket {
public static void main(String[] args) {
final Ticket t = new Ticket();
// lambda表达式
new Thread(()->{for (int i=0;i<20;i++){t.sale();}},"AA").start();
new Thread(()->{for (int i=0;i<20;i++){t.sale();}},"BB").start();
new Thread(()->{for (int i=0;i<20;i++){t.sale();}},"CC").start();
// new Thread(new Runnable() {
// public void run() {
// for (int i=0;i<20;i++){
// t.sale();
// }
// }
// },"窗口1:").start();
// new Thread(new Runnable() {
// public void run() {
// for (int i=0;i<20;i++){
// t.sale();
// }
// }
// },"窗口2:").start();
// new Thread(new Runnable() {
// public void run() {
// for (int i=0;i<20;i++){
// t.sale();
// }
// }
// },"窗口3:").start();
}
}
/**
* 资源类
*/
class Ticket{
Ticket(){}
private int number = 10; // 剩余票数
private Lock lock = new ReentrantLock();
/**
* 操作
*/
public void sale(){
try {
lock.lock();
if(number>0){
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+ "卖票:"+number);
number--;
}else {
System.out.println("卖完了");;
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
五、java8特性
lambda表达式
什么是lambda表达式:左边小括号里写参数,右边写具体实现
写法:拷贝小括号(),写死右箭头->,落地大括号{…}
要求:接口只有一个方法时,方法名可省略
函数式接口:@FunctionalInterface:接口内只允许写一个方法
六、线程间通信
面试题
两个线程,一个线程打印1-52,另一个打印字母A-Z,打印顺序为12A34B…5152Z。要求用线程间通信
/**
* 两个线程,一个线程打印1-52,另一个打印字母A-Z,打印顺序为12A34B…5152Z。要求用线程间通信
*/
public class T01 {
public static void main(String[] args) {
ShareDataOne shareDataOne = new ShareDataOne();
new Thread(()->{
try {
shareDataOne.printNumber();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
shareDataOne.printWord();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
class ShareDataOne{
public synchronized void printNumber() throws InterruptedException {
for(int i=1;i<=52;i++){
System.out.println(i);
while(i % 2 == 0){
notify();
wait();
}
}
}
public synchronized void printWord() throws InterruptedException{
for(char i = 'A';i<='Z';i++){
System.out.println(i);
notify();
wait();
}
}
}
下面我们用Lock接口,对标实现
public class T02 {
public static void main(String[] args) {
ShareDataOne2 shareDataOne2 = new ShareDataOne2();
new Thread(()->{shareDataOne2.printNumber();}).start();
new Thread(()->{shareDataOne2.printWord();}).start();
}
}
class ShareDataOne2{
final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void printNumber(){
lock.lock();
try {
for(int i=1;i<=52;i++){
System.out.print(i);
if(i % 2 == 0){
condition.signalAll();
condition.await();
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printWord(){
lock.lock();
try {
for (char i = 'A';i<='Z';i++){
System.out.print(i);
condition.signalAll();
condition.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
多线程编程模板中
1、判断
2、干活
3、通知
多线程编程模板下
注意多线程之间的虚假唤醒
线程间定制化通讯
题目:
多线程之间按顺序调用,实现A->B->C
三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次;接着
AA打印5次,BB打印10次,CC打印15次;
…来10轮
实现思路:
1、有顺序的通知,需要有标识位!!!
2、有一把锁Lock,3把钥匙Condition
3、判断标志位
4、输出线程名+第几轮+第几次
5、修改标志位,通知下一个
/**
* 多线程之间按顺序调用,实现A->B->C
* 三个线程启动,要求如下:
* AA打印5次,BB打印10次,CC打印15次;接着
* AA打印5次,BB打印10次,CC打印15次;
* ......来10轮
*/
public class T03 {
public static void main(String[] args) {
ShareDataOne3 shareDataOne3 = new ShareDataOne3();
new Thread(()->{
for (int i=1;i<=10;i++){
shareDataOne3.print5(i);
}
},"AA").start();
new Thread(()->{
for (int i=1;i<=10;i++){
shareDataOne3.print10(i);
}
},"BB").start();
new Thread(()->{
for (int i=1;i<=10;i++){
shareDataOne3.print15(i);
}
},"CC").start();
}
}
class ShareDataOne3{
private Lock lock = new ReentrantLock();
private int num = 1; //1:AA 2:BB 3:CC
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
public void print5(int j){
lock.lock();
try{
// 判断
while (num != 1){
conditionA.await();
}
// 干活
for(int i=1;i<=5;i++){
System.out.println("轮次:"+j+";"+Thread.currentThread().getName()+":"+i);
}
// 通知
num = 2;
conditionB.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print10(int j){
lock.lock();
try{
// 判断
while (num != 2){
conditionB.await();
}
// 干活
for(int i=1;i<=10;i++){
System.out.println("轮次:"+j+";"+Thread.currentThread().getName()+":"+i);
}
// 通知
num = 3;
conditionC.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print15(int j){
lock.lock();
try{
// 判断
while (num != 3){
conditionC.await();
}
// 干活
for(int i=1;i<=15;i++){
System.out.println("轮次:"+j+";"+Thread.currentThread().getName()+":"+i);
}
// 通知
num = 1;
conditionA.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
我们按这个思路,来实现下刚才的面试题,代码如下:
/**
* 两个线程,一个线程打印1-52,另一个打印字母A-Z,打印顺序为12A34B…5152Z。要求用线程间通信
*/
public class T04 {
public static void main(String[] args) {
ShareDataOne4 shareDataOne4 = new ShareDataOne4();
new Thread(()->{shareDataOne4.printNumber();}).start();
new Thread(()->{shareDataOne4.printWord();}).start();
}
}
class ShareDataOne4{
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private int flag = 1; // 1:打印数字 2:打印字母
public void printNumber(){
lock.lock();
try {
// 判断
while ( flag != 1){
condition1.await();
}
// 干活
for(int i=1;i<=52;i++){
System.out.print(i);
if(i % 2 == 0){
// 通知
flag = 2;
condition2.signal();
condition1.await();
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printWord(){
lock.lock();
try {
// 判断
while ( flag != 2){
condition2.await();
}
// 干活
for(char w = 'A';w<='Z';w++){
System.out.print(w);
// 通知
flag = 1;
condition1.signal();
condition2.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
七、NoSafeDemo
7.1 NoSafeList
7.1.1 举例说明ArrayList不安全
证明方式一:看源码,add方法中没有加synchronized关键字
证明方式二:代码模拟多线程读写过程
public class T05 {
public static void main(String[] args) {
List<String> array = new ArrayList<>();
// new CopyOnWriteArrayList();
//new ArrayList<>();
for(int i=0;i<20;i++){
new Thread(()->{
array.add(UUID.randomUUID().toString());
System.out.println(array);
}).start();
}
}
}
运行结果:
7.1.2 解决方案(CopyOnWriteArrayList)
1、Vector效率低
2、Collections效率低
3、用juc的CopyOnWriteArrayList;写时拷贝技术。
CopyOnWriteArrayList是arrayList的一种线程安全变体,其中所有可变操作(add,set等)都是通过生成底层数组的新副本来实现的。
原理:
我们都知道,集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机制,性能较差。而CopyOnWriteArrayList则提供了另一种不同的并发处理策略(当然是针对特定的并发场景)。
很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
源码:
下面看看CopyOnWriteArrayList的add(E e)方法源码
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
7.2 NoSafeMap
7.2.1 举例说明HashMap不安全
public class T05 {
public static void main(String[] args) {
Map<String,String> map = new HashMap();
for(int i=0;i<20;i++){
new Thread(()->{
map.put(UUID.randomUUID().toString(),Thread.currentThread().getName());
System.out.println(map);
}).start();
}
}
}
运行结果:
7.2.2 解决方案(ConcurrentHashMap)
8、八锁问题
1、标准访问,先打印短信还是邮件
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendSMS();
}catch (Exception e){
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
phone.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}
class Phone{
public synchronized void sendSMS(){
System.out.println("---sendSMS---");
}
public synchronized void sendEmail(){
System.out.println("---sendEmail---");
}
}
运行结果:
分析: 因为这里都是phone这个对象去调用的sendSMS和sendEmail方法,所以同步监视器都是当前实例对象this,也就是phone这个对像。由于调用线程AA之后,主线程sleep了0.1秒,所以肯定是线程AA先拿到锁,执行完sendSMS方法之后,才会释放锁让线程BB拿到锁去执行sendEmail方法。所以先打印sendSMS后打印sendEmail。
2、停4秒在短信方法内,先打印短信还是邮件
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendSMS();
}catch (Exception e){
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
phone.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}
class Phone{
public synchronized void sendSMS() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("---sendSMS---");
}
public synchronized void sendEmail(){
System.out.println("---sendEmail---");
}
}
运行结果:
分析: 分析同1,因为线程AA先拿到锁
3、新增普通的hello方法,是先打印短信还是hello
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendSMS();
}catch (Exception e){
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
// phone.sendEmail();
phone.getHello();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}
class Phone{
public synchronized void sendSMS() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("---sendSMS---");
}
public synchronized void sendEmail(){
System.out.println("---sendEmail---");
}
public void getHello(){
System.out.println("---Hello---");
}
}
运行结果:
分析:
因为hello是普通方法,没有锁,所以即使线程AA先拿到锁了,也不影响线程BB执行getHello方法。虽然线程BB晚执行getHello方法0.1秒,但是打印sendSMS前sleep了4秒,所以先打印Hello,后打印sendSMS。
4、现在有两部手机,先打印短信还是邮件
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone.sendSMS();
}catch (Exception e){
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
phone2.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}
class Phone{
public synchronized void sendSMS() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("---sendSMS---");
}
public synchronized void sendEmail(){
System.out.println("---sendEmail---");
}
public void getHello(){
System.out.println("---Hello---");
}
}
运行结果:
分析:
因为sendSMS和sendEmail是被两个不同的Phone对象调用的,所以这两个方法是上个两个不同的锁,所以两个线程分别执行这两个方法不需要抢锁。因为打印sendSMS之前需要sleep 4秒,所以虽然sendEmail方法比sendSMS方法晚执行0.1秒,但显然也是先打印sendEmail后打印sendSMS
5、两个静态同步方法,1部手机,先打印短信还是邮件
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
// Phone phone2 = new Phone();
new Thread(()->{
try {
phone.sendSMS();
}catch (Exception e){
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
phone.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}
class Phone{
public static synchronized void sendSMS() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("---sendSMS---");
}
public static synchronized void sendEmail(){
System.out.println("---sendEmail---");
}
}
运行结果:
分析:
static方法,是类方法,所以其同步监视器是这个类.class这个对象,即Phone.class这个对象,所以这里只有一把锁,因为线程BB晚执行0.1秒,所以线程AA先拿到锁,先打印sendSMS,线程BB后拿到锁,后打印sendEmail。
6、两个静态同步方法,2部手机,先打印短信还是邮件
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone.sendSMS();
}catch (Exception e){
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
phone2.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}
class Phone{
public static synchronized void sendSMS() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("---sendSMS---");
}
public static synchronized void sendEmail(){
System.out.println("---sendEmail---");
}
}
结果:
分析:
static方法,是类方法,同步监视器是类.class,即Phone.class,所以phone和phone2是同一把锁,因为phone先拿到锁,phone2后拿到锁,所以先打印sendSMS,后打印sendEmail。
7、1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
// Phone phone2 = new Phone();
new Thread(()->{
try {
phone.sendSMS();
}catch (Exception e){
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
phone.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}
class Phone{
public static synchronized void sendSMS() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("---sendSMS---");
}
public synchronized void sendEmail(){
System.out.println("---sendEmail---");
}
}
结果:
分析:
不是同一把锁
8、1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone2.sendSMS();
}catch (Exception e){
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
phone.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}
class Phone{
public static synchronized void sendSMS() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("---sendSMS---");
}
public synchronized void sendEmail(){
System.out.println("---sendEmail---");
}
}
结果:
分析:
不是同一把锁
获得多线程的方法有几种?
传统的是继承Thread类和实现Runnable接口;java5以后又有实现callable接口和java的线程池获得