文章目录
- Java并发基础
- 多线程的出现是要解决什么问题的? 本质什么
- Java是怎么解决并发问题的
- 线程安全有哪些实现思路
- 如何理解并发和并行的区别
- 线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?
- 通常线程有哪几种使用方式
- 为什么必须调用Thread start方法,不能直接调用Thread的run方法创建
- 覆写Runnable接口相比继承Thread类的好处
- 基础线程机制有哪些
- 线程的互斥同步方式有哪些? 如何比较和选择
- 线程之间有哪些协作方式
- Synchronized由什么样的缺陷?Java Lock是怎么弥补这些缺陷的?
- Synchronized在使用时有何注意事项?
- Synchronized修饰的方法在抛出异常时,会释放锁吗?
- 多个线程等待同一个Synchronized锁的时候,JVM如何选择下一个获取锁的线程?
- synchronized是公平锁吗?
- volatile关键字的作用是什么
- 32位机器上共享的long和double变量的为什么要用volatile?
- 如何理解private所修饰的方法是隐式的final?
- 说说final类型的类如何拓展?
- final方法可以被重载吗?
- 父类的final方法能不能够被子类重写?
Java并发基础
多线程的出现是要解决什么问题的? 本质什么
CPU、内存、I/O设备的速度是有极大差异的,为了合理利用CPU的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
CPU 增加了缓存,以均衡与内存的速度差异;// 导致可见性问题。
操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异;// 导致原子性问题。
编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致有序性问题。
Java是怎么解决并发问题的
理解的第一个维度:核心知识点
JMM本质上可以理解为,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:
volatile、synchronized和final三个关键字
Happens-Before规则
理解的第二个维度:可见性,有序性,原子性
原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 请分析以下哪些操作是原子性操作:
x = 10; //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
y = x; //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
x++; //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
x = x + 1; //语句4: 同语句3
上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性
在Java里面,可以通过volatile关键字来保证一定的“有序性”。
另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。
线程安全有哪些实现思路
互斥同步
synchronized 和 ReentrantLock
非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
1、CAS:随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有3个操作数,分别是内存地址V、旧的预期值A和新值B。当执行操作时,只有当V的值等于A,才将V的值更新为B。
2、AtomicInteger:J.U.C包里面的整数原子类AtomicInteger,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。
无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
1、栈封闭:多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
2、线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
如何理解并发和并行的区别
并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?
新建(New):创建后尚未启动。
可运行(Runnable):可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态中的 Running 和 Ready。
阻塞(Blocking):等待获取一个排它锁,如果其他线程释放了锁就会结束此状态。
无限期等待(Waiting):等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
限期等待(Timed Waiting):无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
死亡(Terminated):可以是线程结束任务之后自己结束,或者产生了异常而结束。
通常线程有哪几种使用方式
有三种使用线程的方法:
实现 Runnable 接口;
class Dog implements Runnable { //通过实现Runnable接口来实现
int count = 0;
@Override
public void run() { //普通方法
while (true) {
System.out.println("你好,兮动人-" + (++count) + Thread.currentThread().getName());
try {
Thread.sleep(1000);// 休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10) {
break;
}
}
}
}
public class ThreadTest01 {
public static void main(String[] args) {
Dog dog = new Dog();
//dog.start(); //这里不能调用start方法
//创建了Thread对象,把dog对象(实现了Runnable),放入了Thread
Thread thread = new Thread(dog);
thread.start();
}
}
通过Thread来执行Runnable任务,底层使用了【代理模式】:
class Animal {}
class Tiger extends Animal implements Runnable {
@Override
public void run() {
System.out.println("老虎...");
}
}
//线程代理类,模拟了一个极简的Thread类
class ThreadProxy implements Runnable { //可以把Proxy类当做 Thread
private Runnable target = null; // 属性类型是Runnable
@Override
public void run() {
if (target != null) {
target.run();//动态绑定(运行类型是Tiger)
}
}
public ThreadProxy(Runnable target) {
this.target = target;
}
public void start() {
start0();//这个方法是真正实现多线程的方法
}
public void start0() {
run();
}
}
public class ThreadTest02 {
public static void main(String[] args) {
Tiger tiger = new Tiger();
ThreadProxy threadProxy = new ThreadProxy(tiger);
threadProxy.start();
}
}
实现 Callable 接口;
// 创建一个实现Callable的实现类, 可以通过设置泛型,指定call方法返回的类型
class CallableThread implements Callable<Integer> {
// 实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
sum += i;
}
}
return sum;
}
}
public class TestCallable {
public static void main(String[] args) {
// 创建Callable接口实现类的对象
CallableThread callableThread = new CallableThread();
// 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask<Integer> futureTask = new FutureTask<>(callableThread);
// 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
// 获取Callable中call方法的返回值(因为会等待线程结束后再获取,所以可以当作闭关锁使用)
// get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
try {
Integer sum = futureTask.get();
System.out.println("计算sum = " + sum);
} catch (Exception e) {
e.printStackTrace();
}
}
}
继承 Thread 类;
public class ThreadByExtend extends Thread{
public static volatile int count = 0;
//Thread真正执行的代码段
public void run() {
for(int i=0;i<50;i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "sold "+(i+1)+" tickets total Sold:" + (++count));
}
}
public static void main(String[] argv) throws InterruptedException{
ThreadByExtend myThread1 = new ThreadByExtend();
ThreadByExtend myThread2 = new ThreadByExtend();
myThread1.start();
myThread2.start();
myThread1.join();
myThread2.join();
}
}
线程池实现
public class ThreadPoolCallable implements Callable {
private static int count = 0;
public String call() {
for(int i=0;i<50;i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "sold "+(i+1)+" tickets total Sold:" + (++count));
}
return "sale out";
}
public static void main(String[] argv) throws InterruptedException, ExecutionException {
ExecutorService ex = Executors.newFixedThreadPool(4);
for (int i = 0; i < 4; i++) {
ex.submit(new ThreadPoolCallable());
}
ex.shutdown();
}
}
public class ThreadPoolRunnable implements Runnable{
private static volatile AtomicInteger count = new AtomicInteger(0);
public void run() {
for(int i=0;i<50;i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sold "+(i+1)+" tickets total Sold:" + count.incrementAndGet());
}
}
public static void main(String[] argv) throws InterruptedException, ExecutionException {
ExecutorService ex = Executors.newFixedThreadPool(4);
for (int i = 0; i < 4; i++) {
ex.submit(new ThreadPoolRunnable());
}
ex.shutdown();
}
}
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
为什么必须调用Thread start方法,不能直接调用Thread的run方法创建
Thead的run方法相当于主线程的main方法,创建线程必须由系统通过start方法来调用。
start方法是一种native方法,即本地方法,start方法通过系统调用注册新的线程,安排好上下文, 才会执行run方法。
如果直接执行run方法,则仍然是在主线程中,将run作为一个普通的方法调用,返回后仍然是在主线程。
每一个Thread对象的start方法只能调用一次。
覆写Runnable接口相比继承Thread类的好处
1、覆写Runnable接口实现多线程可以避免单继承局限,因为我们的线程工作对象可能是别的类的子类,那么强制必须是Thread类的子类就会带来很大的麻烦。
2、通过runnable方式,多个线程可以共享同一个runnable对象,大家可以对比一下,我们在继承Thread的方式中,线程之间因为是不同的对象,那么共享数据就只能用静态变量的方式才能使得多个线程之间可见。而在runnable方式的实现中,因为不同的thread的构造都是同一个runnable对象,所以runnable中的数据是共享的,不需要静态。
基础线程机制有哪些
Executor:Executor管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。
主要有三种Executor:
CachedThreadPool:一个任务创建一个线程;
FixedThreadPool:所有任务只能使用固定大小的线程;
SingleThreadExecutor:相当于大小为1的FixedThreadPool;
Daemon:
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
main()属于非守护线程。使用setDaemon()方法将一个线程设置为守护线程。
sleep():
Thread.sleep(millisec)方法会休眠当前正在执行的线程,millisec单位为毫秒。
sleep() 可能会抛出InterruptedException,因为异常不能跨线程传播回main()中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
yield():
对静态方法Thread.yield()的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
线程的互斥同步方式有哪些? 如何比较和选择
Java提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的synchronized,而另一个是JDK实现的ReentrantLock。
1.锁的实现:
synchronized是JVM实现的,而ReentrantLock是JDK实现的。
2.性能:
新版本Java对synchronized进行了很多优化,例如自旋锁等,synchronized与ReentrantLock大致相同。
3.等待可中断:
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock可中断,而synchronized不行。
4.公平锁:
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但是也可以是公平的。
5.锁绑定多个条件:
一个ReentrantLock可以同时绑定多个Condition对象。
线程之间有哪些协作方式
join():在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
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();
}
}
虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
wait()、notify()、notifyAll():
1、wait()的作用是使当前执行wait()方法的线程等待,在wait()所在的代码行处暂停执行,并释放锁,直到接到通知或中断。
2、notify()方法用来通知那些可能等待该锁的其他线程,如果有多个线程等待,则按照执行wait()方法的顺序发出一次性通知(一次只能通知一个!),使得等待排在第一顺序的线程获得锁。
需要说明的是,执行notify方法后,当前线程并不会立即释放锁,要等到程序执行完,即退出synchronized同步区域后。
3、它们都属于Object的一部分,而不属于Thread。只能用在同步方法或者同步控制块中使用,否则会在运行时抛出IllegalMonitorStateExeception。
4、wait/notify在调用前一定要获得相同的锁,如果在调用前没有获得锁,程序会抛出异常,也就调用不了wait/notify;另外,如果获得的不是同一把锁,notify不起作用。
代码示例:
public class Service {
Object lock = new Object();
public void waitMethod() {
synchronized(lock) {
try {
System.out.println(Thread.currentThread().getName()+"执行了wait方法,释放了锁");
lock.wait();
System.out.println(Thread.currentThread().getName()+"被唤醒了");
}catch (Exception e) {
e.printStackTrace();
}
}
}
public void notifyMethod() {
synchronized(lock) {
try {
System.out.println(Thread.currentThread().getName()+"执行了notify方法");
lock.notify();;
System.out.println(Thread.currentThread().getName()+"继续执行notify后的代码,完事后才释放锁");
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
super.run();
service.waitMethod();
}
}
public class ThreadB extends Thread {
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
super.run();
service.notifyMethod();
}
}
public class Test {
public static void main(String[] args) throws Exception {
Service service = new Service();
ThreadA threadA= new ThreadA(service);
ThreadB threadB= new ThreadB(service);
threadA.setName("A");
threadB.setName("B");
threadA.start();
threadB.start();
}
}
// 执行结果:
A执行了wait方法,释放了锁。
B执行了notify方法。
B继续执行notify后的代码,完事后才释放锁。
A被唤醒了。
// 1、线程ThreadA启动后,获得lock锁,成功执行了wait方法,在wait方法所在行暂停,并且释放了锁
// 2、线程ThreadB获得ThreadA释放的锁,成功执行了notify方法并且通知到了ThreadA,告诉它做好苏醒准备,但ThreadB并没有马上释放锁,它继续执行,直到退出synchronzied 代码块
// 3、ThreadB退出synchronized 代码块后释放锁,ThreadA获得锁,从暂停的地方往后执行,直到程序执行完毕
wait()和sleep()的区别
wait()是Object的方法,而sleep()是Thread的静态方法。
wait()会释放锁,sleep()不会。
await()、signal()、signalAll()
java.util.concurrent类库中提供了Condition类来实现线程之间的协调,可以在Condition上调用await()方法使线程等待,其它线程调用signal()或signalAll()方法唤醒等待的线程。相比于wait()这种等待方式,await()可以指定等待的条件,因此更加灵活。
代码示例:
public class SweepAndMopDispacherNew {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean isSweeping = false;
public void sweeped() {
lock.lock();
try {
isSweeping = true;
condition.signalAll();
} finally {
lock.unlock();
}
}
public synchronized void Mopped() {
lock.lock();
try {
isSweeping = false;
condition.signalAll();
} finally {
lock.unlock();
}
}
public void waitForSweeped() throws InterruptedException {
lock.lock();
try {
while (isSweeping == false) {
condition.await();
}
} finally {
lock.unlock();
}
}
public void waitForMopped() throws InterruptedException {
lock.lock();
try {
while (isSweeping == true) {
condition.await();
}
} finally {
lock.unlock();
}
}
}
public class SweepRunner implements Runnable {
private SweepAndMopDispacherNew dispacher;
public SweepRunner(SweepAndMopDispacherNew dispacher) {
this.dispacher = dispacher;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println(Thread.currentThread().getName() + "扫地..");
TimeUnit.MILLISECONDS.sleep(800);
dispacher.sweeped();
dispacher.waitForMopped();
}
} catch (InterruptedException e) {
System.out.println("exit by interrupted");
} finally {
System.out.println("SweepRunner exit;");
}
}
}
public class MopRunner implements Runnable {
private SweepAndMopDispacherNew dispacher;
public MopRunner(SweepAndMopDispacherNew dispacher) {
this.dispacher = dispacher;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println(Thread.currentThread().getName() + "拖地..");
TimeUnit.MILLISECONDS.sleep(800);
dispacher.Mopped();
dispacher.waitForSweeped();
}
} catch (InterruptedException e) {
System.out.println("exit by interrupted");
} finally {
System.out.println("MopRunner exit;");
}
}
}
public class TestMain {
public static void main(String[] args) {
SweepAndMopDispacherNew dispacher = new SweepAndMopDispacherNew();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(new SweepRunner(dispacher));
executorService.submit(new MopRunner(dispacher));
try {
TimeUnit.SECONDS.sleep(5);
executorService.shutdownNow();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Synchronized由什么样的缺陷?Java Lock是怎么弥补这些缺陷的?
synchronized的缺陷
效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时。
不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活。
无法知道是否成功获得锁:相对而言,Lock可以拿到状态。
Lock类这里不做过多解释,主要看里面的4个方法:
lock():加锁。
unlock():解锁。
tryLock():尝试获取锁,返回一个boolean值。
tryLock(long,TimeUtil):尝试获取锁,可以设置超时。
Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。
Synchronized在多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。
ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。
Synchronized在使用时有何注意事项?
锁对象不能为空,因为锁的信息都保存在对象头里。
作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错。
避免死锁
在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错。
Synchronized修饰的方法在抛出异常时,会释放锁吗?
会。
多个线程等待同一个Synchronized锁的时候,JVM如何选择下一个获取锁的线程?
非公平锁,即抢占式。
synchronized是公平锁吗?
synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象。
volatile关键字的作用是什么
1、防重排序
我们从一个最经典的例子来分析重排序问题。大家应该都很熟悉单例模式的实现,而在并发环境下的单例实现方式,我们通常可以采用双重检查加锁(DCL)的方式来实现。
代码示例:
public class Singleton {
private static volatile Singleton singleton;
// 构造函数私有,禁止外部实例化
private Singleton() {};
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
现在我们分析一下为什么要在变量singleton之间加上volatile关键字。要理解这个问题,先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:
1、分配内存空间。2、初始化对象。3、将内存空间的地址赋值给对应的引用。
但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
1、分配内存空间。2、将内存空间的地址赋值给对应的引用。3、初始化对象。
如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。
2、实现可见性
可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile关键字能有效的解决这个问题,代码示例:
public class TestVolatile {
private static boolean stop = false;
public static void main(String[] args) {
// Thread-A
new Thread("Thread A") {
@Override
public void run() {
while (!stop) { }
System.out.println(Thread.currentThread() + " stopped");
}
}.start();
// Thread-main
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread() + " after 1 seconds");
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
}
}
输出:
Thread[main,5,main] after 1 seconds
// Thread A一直在loop, 因为Thread A由于可见性原因看不到Thread Main已经修改stop的值
// 如果通过在stop变量前面加上volatile关键字则会真正stop
输出:
Thread[main,5,main] after 1 seconds
Thread[Thread A,5,main] stopped
32位机器上共享的long和double变量的为什么要用volatile?
因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。
目前各种平台下的商用虚拟机都选择把64位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不把long和double变量专门声明为volatile多数情况下也是不会错的。
如何理解private所修饰的方法是隐式的final?
类中所有private方法都隐式地指定为final的,由于无法取用private方法,所以也就不能覆盖它。
public class Base {
private void test() {
}
}
public class Son extends Base{
public void test() {
}
public static void main(String[] args) {
Son son = new Son();
Base father = son;
//father.test();
}
}
// Base和Son都有方法test(),但是这并不是一种覆盖,因为private所修饰的方法是隐式的final,也就是无法被继承,所以更不用说是覆盖了,在Son中的test()方法不过是属于Son的新成员罢了,Son进行向上转型得到father,但是father.test()是不可执行的,因为Base中的test方法是private的,无法被访问到。
说说final类型的类如何拓展?
比如String是final类型,我们想写个MyString复用所有String中方法,同时增加一个新的toMyString()的方法,应该如何做?
使用外观模式:
/**
* @pdai
*/
class MyString{
private String innerString;
// ...init & other methods
// 支持老的方法
public int length(){
return innerString.length(); // 通过innerString调用老的方法
}
// 添加新方法
public String toMyString(){
//...
}
}
final方法可以被重载吗?
父类的final方法是不能够被子类重写的,那么final方法可以被重载吗? 答案是可以的,下面代码是正确的。
public class FinalExampleParent {
public final void test() {
}
public final void test(String str) {
}
}
父类的final方法能不能够被子类重写?
不可以。