本文是一篇练习总结,记录在学习下面这份博客时的问题,并对一些相关问题进行归纳。
1. wait和sleep的区别
- 这两个方法来自不同的类分别是Thread和Object
- 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
- sleep是Thread类的静态方法。sleep的作用是让线程休眠制定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件事恢复线程执行。wait是Object的方法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify方法才会重新激活调用者。
通信方法 | 描述 |
Thread.sleep(100) | sleep的作用是让线程休眠制定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件事恢复线程执行 |
wait() | 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。wait是Object的方法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify方法才会重新激活调用者。 |
notify | 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程 |
notifyAll | 一旦执行此方法,就会唤醒所有被wait()的线程 |
以下为题目思考部分:
- 这是一个多线程的问题,在学习这个问题时首先我们要对java的多线程有了解,参考博文:多线程的练习。在该博文中使用了两种方法来理解多线程问题:
- ① 继承Thread类的方法: 这种方法使用wait和sleep函数。
- 首先,创建User即创建线程,不同线程使用同一个Account。重写User的构造方法和run方法,从而决定线程的操作行为。
- 其次,创建Account,在Account内处理线程同步问题。在Account中需要进行线程同步的方法为cunqian()方法。使用用同步代码块的方式解决线程安全问题,同步监视器为Account.class。
- 关键代码行见下:(从中可以看出sleep和wait的区别) 这里使用notify和wait的作用是保证进程执行顺序按照先“甲”后“乙”的顺序。而synchronized的作用是线程同步,保证线程安全的。如果去掉notify和wait,只是甲乙存款的顺序会乱掉,但是由于synchronized,所以仍然可以保证线程安全。
//存钱方法
public void cunqian(double jine){
if(jine>0){
//用同步代码块的方式解决线程安全问题,同步监视器为Account.class
synchronized (Account.class){
Account.class.notify();
money+=1000;
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-存款,账户余额为:"+money);
//
try{
Account.class.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
- 实现Runnable接口:
- 更多线程问题,可以参考:深入理解线程问题
2. synchronized底层原理是可重入锁吗
- 重入锁:所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
- synchronized 和 ReentrantLock 都是可重入锁。
- 可重入锁的意义在于防止死锁。
- 实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
- 关于父类和子类的锁的重入:子类覆写了父类的synchonized方法,然后调用父类中的方法,此时如果没有重入的锁,那么这段代码将产生死锁
- JVM基于进入和退出Monitor对象来显示方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法的结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit与之配对。
- Synchronize是可重入锁,一个获得锁的线程可以再次进入一个同步块方法
- 参考链接:可重入锁和不可重入锁。根据链接中的代码例子,我们可以很容易地理解重入锁和不可重入锁。关键在于notify()执行在锁的数量为0时(数量大于0都不notify),还是无锁时(锁的数量只能为1或0)。
3. CAS原理,CAS会有什么问题,怎么解决ABA问题(可以谈Java内存模型)
CAS:Compare and Swap。 CAS的概念与synchronized相对。CAS为乐观锁,而synchronized则为悲观锁。
- CAS(乐观锁的一种):
- 乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测。
- 如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
- 参考链接:CAS原理分析,根据链接中的代码,可以很方便地理解CAS问题。
synchronized(悲观锁)的问题:
- 在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题
- 如果一个线程持有锁,其他的线程就都会挂起,等待持有锁的线程释放锁。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能风险。
CAS的缺陷:
- ABA问题:虽然出栈了,但是其指针指向的地址还是不变。
- 循环时间长,开销大
- 只能保证一个变量的原子操作
CAS的本质是首先检测冲突,其次如果检测到冲突就更新数据。而ABA问题正是因为冲突检测的机制容易被欺骗。
解决ABA问题:版本号,JDK的Atomic包里提供了一个类Atomic包里提供了一个AtomicStampedReference来解决ABA问题
4. HashMap数据结构线程安全吗,举个例子HashMap怎么线程不安全
不安全,在resize的时候,两个线程同时操作会出现链表成环问题,导致查找一个不存在的key的时候,CPU利用率达到100%,解决方法使用ConcurrentHashMap。 另,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。
参考链接:hashmap是线程安全的吗?怎么解决?
5. java的基本数据类型和字节数
参考链接:short、int、Integer、long (1字节byte=8bit)
整型 | 浮点型 | 字符 | 布尔 |
byte(1字节) | 浮点型:float(4字节) | char(2字节) | boolean(1位) |
short(2字节) | double(8字节) | ||
int(4字节) | |||
long(8字节) |
6. Java,volatile关键字
- volatile 是 java 提供的最轻量级的同步机制,当变量定义为 volatile 后,可以保证此变量对多线程的可见性。多个线程可以读到内存中最新的值。
- volatile 可以保证可见性和有序性,不能保证原子性。
- 根据总线上的嗅探机制修改共享变量会使其他线程中的缓存变量失效,但不是立即从主内存中更新值,而是等到下一次use的时候,发现值是无效的,才会从主内存中读取新的值。
更多关于并发编程可以参考:java并发编程,volatile关键字。
在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:
- 原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性:即程序执行的顺序按照代码的先后顺序执行。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
7.String,StringBuffer,StringBuilder区别
String | StringBuilder | StringBuffer |
- - | StringBuilder不是线程安全的 | StringBuffer是线程安全的 |
- - | 有速度优势 | |
Java 中字符串属于对象,String的值是不可变的,这就导致每次对String的操作都会生成新的String对象。 | 当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。 |
8. Concurrent下面的包的原理
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
- 首先,声明共享变量为volatile;
- 然后,使用CAS的原子条件更新来实现线程之间的同步;
- 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。