java线程&线程中并发安全
一.线程的基础知识
1.线程与进程的区别
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在
指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
一个进程之内可以分为一到多个线程。
进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
2.并行与并发的区别
单核CPU下线程实际还是串行执行的
操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。
总结为一句话就是: 微观串行,宏观并行
一般会将这种线程轮流使用CPU的做法称为并发(concurrent)
时间片1 时间片2 时间片3
core1 线程1 线程2 线程3
多cpu:每个核(core)都可以调度运行线程,这时候线程可以是并行的
时间片1 时间片2 时间片3
core1 线程1 线程2 线程3
core2 线程1 线程2 线程3
并行和并发有什么区别?
并发(concurrent)是同一时间应对(dealing with)多件事情的能力,同一时间的应对能力
并行(parallel)是同一时间动手做(doing)多件事情的能力,多件事同时进行
现在都是多核CPU,在多核CPU下
并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
3.线程创建的方式有哪些?
共有四种方式可以创建线程,分别是:
3.1继承Thread类:
class MyThread extends Thread{
MyThread t1=new MyThread();
MyThread t2=new MyThread();
t1.start();
t2.start();
}
3.2实现runnable接口
class MyRunnable implements Runnable{
MyRunnable mr=new MyRunnable();
Thread t1=new Thread(mr);
Thread t2=new Thread(mr);
t1.start();
t2.start();
}
3.3实现Callable接口
MyCallable implemnets Callable
MyCallable mc=new MyCallable();
FutureTask
Thread t1 = new Thread(ft) ;
Thread t2 = new Thread(ft) ;
t1.start();
t2.start();
String result = ft.get();
}
3.4线程池创建线程
MyExecutors implements Runnable{
// 创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors()) ;
// 关闭线程池
threadPool.shutdown();
}
3.5 runnable 和 callable 有什么区别
Runnable 接口run方法没有返回值
Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
3.6线程的 run()和 start()有什么区别?
start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run(): 封装了要被线程执行的代码,可以被调用多次。
3.7 线程包括哪些状态,状态之间是如何变化的
线程的状态可以参考JDK中的Thread类中的枚举State
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待( WAITING )、时间等待(TIMED_WALTING)、终止(TERMINATED)
创建线程对象是新建状态
调用了start()方法转变为可执行状态
线程获取到了CPU的执行权,执行结束是终止状态
在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
3.8新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
可以使用线程中的join方法解决
t1.join(); // t2加入线程t1,只有t1线程执行完毕以后,再次执行该线程t2; t3 加入线程t2,t2.join
3.9 notify()和 notifyAll()有什么区别?
notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个 wait 线程
3.10在java中wait和sleep方法的不同
共同点
wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
1.方法归属不同
sleep(long) 是 Thread 的静态方法
而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
2.醒来时机不同
执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
它们都可以被打断唤醒
3. 锁特性不同(重点)
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
3.11 如何停止一个正在运行的线程
有三种方式可以停止线程
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
使用stop方法强行终止(不推荐,方法已作废)
使用interrupt方法中断线程
打断阻塞的线程( sleep,wait,join )的线程,线程会抛出InterruptedException异常
打断正常的线程,可以根据打断状态来标记是否退出线程
二.线程中并发安全
2.1 synchronized关键字的底层原理
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住
2.2请谈谈你对 volatile 的理解
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
① 保证线程间的可见性
② 禁止进行指令重排序
2.3 synchronized和Lock有什么区别 ?
语法层面
synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
Lock 是接口,源码由 jdk 提供,用 java 语言实现
使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量
Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁)
性能层面
在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
在竞争激烈时,Lock 的实现通常会提供更好的性能
2.4 死锁产生的条件是什么?
一个线程需要同时获取多把锁,这时就容易发生死锁
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
jps:输出JVM中运行的进程状态信息
jstack:查看java进程内线程的堆栈信息,查看日志,检查是否有死锁
如果有死锁现象,需要查看具体代码分析后,可修复
可视化工具jconsole、VisualVM也可以检查死锁问题
2.5导致并发程序出现问题的根本原因是什么
1.原子性 synchronized、lock
2.内存可见性 volatile、synchronized、lock
3.有序性 volatile