1.
线程简介
在操作系统中执行某一个功能,如听歌,看电影,那么就必须在操作系统中执行相应的软件。
软件本身是由某种编程语言而编写的指令的集合,所以执行软件,就等同于执行软件中的程序语言。
当操作系统执行这个软件时,会分配一定的内存空间并进行内存调度来运行程序,软件关闭时,操作系统会回收之前分配的内存。我们将软件执行的这个过程,称之为
进程
。
所以简单来讲,启动软件,就等同于启动了一个进程。
启动软件后,软件可能需要执行数据库交互,缓存数据存储或展示用户页面等操作,这些操作是同时执行,而不是一个执行完了后另外一个执行。每一个操作就等同于在当前软件进程中创建了一个执行路径,执行时,软件会按照这个路径进行操作,我们将这样的执行路径称之为
线程
。
软件执行时,会将操作系统分配的内存再次进行分配,分配给不同的线程,某一时刻只有一个线程有CPU的执行权,其他线程可能在等待(就绪),或者被阻塞,因为CPU非常快,所以给这线程的分配的时间非常短,这个时间一到,立马会把当前的线程切换一个状态,另一个等待的线程抢到CPU的执行权。
有的进程中只有一个线程,所以我们有时也将线程称之为
轻量级进程
(Lightweight Process,LWP)
JAVA执行程序中,会自动创建主线程(main),在主线程中调用其它的业务逻辑代码。
线程的使用,主要是为了抢占资源,所以开发程序时,应该用面向对象的方式考虑资源和线程的区别。编写相应的代码,根据实际的场景选择是否使用同步处理。
2.
JUC
简介
从Java 5.0开始,java 提供了java.util.concurrent(简称JUC )包,在此包中增加了在
并发编程
中很常用的
实用工具类
,用于定义类似于线程的自定义子系统,包括
线程池
、异步IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的Collection 实现等
。
3.
线程状态
4.
创建线程
继承父类Thread
实现接口Runnable
匿名内部类
Lambda表达式(jdk1.8)
实现接口Callable
线程池Executors的工具类
5.
线程常用方法
类&接口
|
方法
|
作用
|
Java.lang.Thread
|
start
|
启动线程,让线程处于就绪状态
|
Java.lang.Thread
|
stop
|
停止线程,方法存在问题,所以不推荐使用
|
Java.lang.Thread
|
sleep
|
休眠线程,
线程的静态方法
休眠时,不释放对象监听器(锁)
|
Java.lang.Thread
|
join
|
将指定的线程加入到当前线程中,只有当指定的线程运行完毕后,当前的线程才能继续执行。
|
Java.lang.Thread
|
yield
|
放弃当前线程的运行,让其他相同优先级的线程执行,但是方法本身存在问题,所以不推荐使用。
|
Java.lang.Object
|
wait
|
线程等待
线程在等待时,会释放对象监听器(锁)
|
Java.lang.Object
|
notify
|
通知
|
Java.lang.Object
|
notifyAll
|
通知其他所有线程
|
|
synchronized
|
同步关键字
|
6.
线程很难
内存可见性
1) 每一个线程都有自己独立的工作内存,执行数据访问时,都会从主存中复制数据,然后快速访问,操作完毕后,将数据放回到主存中。但是工作内存之间无法直接交互,我们管这样的现象称之为内存可见性问题。
2) 如果增加同步关键字,那么内存操作中,会首先将工作内存清空,重新获取主存的数据然后再加锁。如果释放锁,会将工作内存的数据同步到主存中。
3)
Volatile
关键字可以修饰变量,那么在访问这个变量时,都会从主存中获取。同时也不会使用JIT即时编译器优化字节码。
|
对象监听器 monitor
1) 每一个对象都有自己的监听器,当执行同步操作时,会给当前对象增加监听器,就是所谓的给对象加锁。
2) Wait,notify,notifyAll方法应该和对象监听器(锁)发生关联,如果没有关联,会出现错误:
java.lang.IllegalMonitorStateException
|
虚假唤醒 spurious wakeup
当线程经过等待后唤醒,不再判断条件,这样就会导致数据安全问题,这个问题,称之为虚假唤醒。
解决方案就是在wait之前的判断时进行条件的循环判断处理。
|
线程八锁(
同步关键字的八种使用场景
)
1. 一个对象,两个同步方法,打印的顺序
两个线程执行不同的方法,哪一个先打印,取决于线程调度机制
。
2. 一个对象,两个同步方法,在一个方法中睡眠4秒,打印的顺序
当调用同步方法时,等同于给当前的对象加锁,其他线程运行时,会判断是否对象有锁,如果有锁的话,那么线程需要等待
3. 一个对象,一个同步方法,一个非同步方法,打印顺序
当调用非同步方法时,线程不需要等待,不需要判断对象是否锁定。继续执行业务逻辑
4. 两个对象,两个同步方法,打印的顺序
同步关键字是对对象加锁,不同对象,加锁是不一样,线程之间不需要等待。
5. 一个对象,两个静态同步方法,打印的顺序
静态同步方法加锁,等同于给
方法区内存对象
加锁(Class),静态方法的监听器是方法区的,而非静态的方法监听器是堆,那个先执行要看谁先抢占资源
6. 两个对象,两个静态同步方法,打印的顺序
两个对象(类型不相同)调用静态同步方法时,会给方法区不同的内存对象加锁,所以多个线程不需要等待。
两个对象(类型相同)调用静态方法,等同于给方法区相同的内存对象加锁,所以多个线程需要等待
7. 一个对象,一个静态同步方法,一个成员同步方法,打印顺序
静态同步方法是获取方法区中内存对象的锁,而成员同步方法获取的是堆内存中对象的锁,锁不是同一个,所以多个线程不需要等待
8. 两个对象,一个静态同步方法,一个成员同步方法,打印顺序
如果两个对象的类型不一样,效果等同于7.
如果两个对象的类型一样,效果依然等同于7
|
7.
线程高级(JUC)
Callable
问:和Runnable接口的区别?
1) Callable是juc中提供的接口,在jdk1.8后,和Runnable接口都是函数式接口。
2) Runnable接口中的方法没有返回值并且不会抛出异常,Callable接口中的方法有返回值且会抛出异常,可以进行统一异常处理,Callable接口支持泛型操作
3) Callable接口无法通过Thread类直接访问运行,必须通过FutureTask类进行转换后在线程中运行
4) Callable接口中方法的返回值可以通过FutureTask对象的get方法获取
5) FutureTask对象的get方法会阻塞当前线程的执行,然后计算Callable接口方法的逻辑处理结果,只有结果返回了,当前的线程会继续执行。
6) Callable接口可以用于实现线程闭锁操作,也可以使用
CountDownLatch
类实现
问:如何关联两个无关系的对象?(下面是我自己写的)
他们是通过FutureTask这个类来关联Runnable和Callable,因为FutureTask实现了RunnableFuture的接口,而RunnableFuture继承了Runnable的接口,而在FutureTask里面有两个构造器,一个是传一个Callable的对象,还有一个是传一个Runnable的对象,这样就可以成功的把Callable转换成Runnable的,Callable 县城启动也是通过Thread的start的方法来实现的,new Thread(new FutureTask(new Callable<Integer>())).start();
|
Lock
问:和synchronized的区别?
1) Synchronized是java中关键字,给对象加锁,所以系统级别的加锁,用户控制不了的。Lock是juc中提供的接口,给对象加锁,由于是用户自己创建出来的锁,所以可以自己控制
2) Synchronized加锁后,必须获取监听器才可以执行,否则需要等待,无法中断,但是lock可以加锁等待时中断
3) Synchronized释放锁的顺序,先加锁的操作后释放锁,lock接口中的释放锁没有顺寻操作,可以自定义
4)
Lock在使用时,不能同时使用wait,notify。NotifyAll方法,否则会出现错误
|
Condition:
增加条件操作,用于替代早期的同步处理中使用的wait,notify方法
Await è wait
signal è notify
signalAll è notifyAll
|
TimeUnit :
时间单元,可以将时间控制的更加准确,容易识别
|
Volatile :
可以增加对象的内存可见性
,
可以不让JIT即时编译器优化字节码,但是无法解决数据的原子性问题
AtomicInteger : 原子类
CAS算法:CompareAndSwap算法
ABA问题:AèBèA
|
8.
线程应用
线程接力
案例:第一个线程打印5次,第二个线程打印10次,第三个线程打印15次,第三个线程执行完毕后,再从第一个线程继续打印,执行第2轮的操作,总共执行10轮
package
com.atguigu;
import
java.util.MissingFormatArgumentException
;
import
java.util.concurrent.locks.Condition;
import
java.util.concurrent.locks.Lock;
import
java.util.concurrent.locks.ReentrantLock;
/**
*
*/
class
Loop{
private
int
flg
=1;
private
Lock
lock
=
new
ReentrantLock();
private
Condition
c1
=
lock
.newCondition();
private
Condition
c2
=
lock
.newCondition();
private
Condition
c3
=
lock
.newCondition();
public
void
print1(){
lock
.lock();
try
{
while
(
flg
!=1){
try
{
c1
.await();
}
catch
(InterruptedException
e
) {
//
TODO
Auto-generated catch block
e
.printStackTrace();
}
}
c2
.signal();
for
(
int
i
= 1;
i
<=5;
i
++) {
System.
out
.println(Thread.
currentThread
().getName()+
i
);
}
flg
=2;
}
finally
{
lock
.unlock();
}
}
public
void
print2(){
lock
.lock();
try
{
while
(
flg
!=2){
try
{
c2
.await();
}
catch
(InterruptedException
e
) {
//
TODO
Auto-generated catch block
e
.printStackTrace();
}
}
c3
.signal();
for
(
int
i
= 1;
i
<=10;
i
++) {
System.
out
.println(Thread.
currentThread
().getName()+
i
);
}
flg
=3;
}
finally
{
lock
.unlock();
}
}
public
void
print3(){
lock
.lock();
try
{
while
(
flg
!=3){
try
{
c3
.await();
}
catch
(InterruptedException
e
) {
//
TODO
Auto-generated catch block
e
.printStackTrace();
}
}
c1
.signal();
for
(
int
i
= 1;
i
<=15;
i
++) {
System.
out
.println(Thread.
currentThread
().getName()+
i
);
}
flg
=1;
}
finally
{
lock
.unlock();
}
}
}
public
class
TestJUC_loop_1 {
public
static
void
main(String[]
args
) {
final
Loop
l
=
new
Loop();
Thread
t1
=
new
Thread(
new
Runnable(){
public
void
run() {
for
(
int
i
= 1;
i
<=10;
i
++) {
l
.print1();
}
}
},
"第一个线程"
);
t1
.start();
Thread
t2
=
new
Thread(
new
Runnable(){
public
void
run() {
for
(
int
i
= 1;
i
<=10;
i
++) {
l
.print2();
}
}
},
"第二个线程"
);
t2
.start();
Thread
t3
=
new
Thread(
new
Runnable(){
public
void
run() {
for
(
int
i
= 1;
i
<=10;
i
++) {
l
.print3();
}
}
},
"第三个线程"
);
t3
.start();
}
}
|
读写锁 (ReadWriteLock)
案例:(红蜘蛛屏幕共享软件)100个线程读取,1个线程写入
案例:多个线程访问缓存,如果缓存中没有,读取数据库,如果有,直接获取。
|
线程池 ( Executors )
案例:固定线程池打印20次业务操作
案例:单一线程池打印20次业务操作
案例:按需线程池打印20次业务操作
案例:让线程池支持时间调度
|
9.
线程面试题
线程有几种状态?之间是如何切换的?
有五种状态:new,就绪,运行,消亡,阻塞
New:Thread t=new Thread(new Runnable(){
Public void run(){
}
});
New-就绪:t.start();
就绪-运行:抢占cpu,运行
运行-消亡:有异常、线程执行完成、error错误
运行-阻塞:join插入,sleep,wait(synchronized),
阻塞-就绪:join的线程执行完成,sleep睡觉完成、wait被notify或notifyAll唤醒
运行-就绪:礼让,cpu被抢占,
|
Java中的volatile关键字是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?
Volatile他使数据为可见数据,他是从主存里面读取数据,保持了数据的可见性,而且JIT(just in time)即时编译器不进行优化字节文件(int i=10;i=20;i=30),但是他不能保证原子性(原子性就是要么都执行,要么都不执行,例如i++);他不是安全的
Volatile是修饰变量的
Synchronized是一个关键字,他可以修饰方法和在同步代码块上,
Synchronized也有数据可见性,他是先把线程私有的内存清空后,在主存里面拿数据,在加一把锁,当线程操作完数据后放入到主存里面后,释放锁的机制,来达到数据的可见性,在这个操作过程中,也会阻塞其他线程操作同样的数据,会影响一些性能,在这方面volatile要好一些,但是这样也保持了原子性操作(其实加锁来实现原子性的);他是安全的
|
Runnable接口和Callable接口的区别
Runnable他只有一个run方法,是@FunctionalInterface修饰的,
Callable他也只有一个方法call,也是@FunctionalInterface注解的
他们都是通过Thread的start方法启动的
他们两个的区别是Callable他有泛型,有返回值,也可以抛出异常
Callable<Integer> c=newCallable<Integer>();
FutureTask<Integer> task=new FutureTask<Integer>(c);
Thraed t=new Thread(task).start();//只用这样才能启动Callable的线程
Thread t=new Thread(new Runnable(){
Public void run(){
})
|
start()方法和run()方法的区别
start()是一个线程的方法,他把new出来的线程启动起来,使线程达到就绪状态,让他有抢占cpu的机会,使他有调度机制调度的机会
而run方法只是一个简单的方法,跟线程已经没有什么关系了
|
sleep方法和wait方法有什么区别
sleep是个静态方法,他是Thread,public static native void sleep(时间);
一个在睡眠时使不会释放锁的,其他线程要访问同一资源时,要等待他执行完成以后才可以
Sleep没有使用情景的要求,Thread.sleep(100);他只要休眠够就可以了(),他和对象没有关系
Wait是一个成员方法,public final void wait();线程在等待的时候,该线程会释放锁的,他只能等notify或者notifyAll来唤醒他,不然该线程会一直等下去,而不会执行(其实等待时间长了,线程也会出现超时,使线程死亡);一般情况下wait和notify、notifyAll、Synchronized一起使用,wait有使用条件的,wait要使用在同步方法后者同步代码块里面
|
ThreadLocal有什么用
他的主要作用就是共享数据,在同一个线程里面,任何对象都可以存数据,也可以取数据,但是他不能解决安全性问题(安全性问题是数据在并发操作是造成的)
|
synchronized和ReentrantLock的区别
synchronized是关键字,他在加锁后是不能被打断的,因为他是系统加的锁机制(jvm),用户是控制不了。而ReetrantLock是Lock接口的实现类,这个类是可以在加锁后,是可以打断的,是受自己控制的,比较灵活,
而ReetrantLock是不能和wait、notify、notifyAll这些搭配使用的,他一般的用Codition这额条件类的方法的await来替代Object类的wait,用signal来替代notify的,signalAll来替代notifyAll,
Lock lock=new ReetrantLock();
Condition c=lock.newCondition();
这个ReetrantLock是一定要自己关闭的,而synchronized是不要我们自己去关闭的,
Synchronized释放锁的顺序是先加的后释放,而ReetrantLock释放锁比较灵活,可以自己定义
|
ConcurrentHashMap的并发度是什么
ConcurrentHashMap是一个线程安全的,他采取的机制是加了分段锁。在put数据时,Segement<k,v> s;
Static final class Segement<k,v> extends ReentrantLock;
为什么ConcurrentHashMap的并发度是16,是因为他默认的segement的容量为16个,
相当于把ConcurrentHashMap分成了16段,每一段的访问都不需要竞争,所以没有并发的操作,不会造成线程等待,大大提高了效率,也解决了安全性问题
并发度是16
|
编写Java代码,解决生产者——消费者问题。
package
com.atguigu;
class
Product{
private
int
num
=0;
public
synchronized
void
produce(){
while
(
num
!=0){
try
{
wait();
}
catch
(InterruptedException
e
) {
e
.printStackTrace();
}
}
num
++;
notifyAll();
System.
out
.println(
"生产了"
+
num
);
}
public
synchronized
void
consume(){
while
(
num
==0){
try
{
wait();
}
catch
(InterruptedException
e
) {
e
.printStackTrace();
}
}
num
--;
notifyAll();
System.
out
.println(
"消费了"
+
num
);
}
}
public
class
Consume_product {
public
static
void
main(String[]
args
) {
Product
p
=
new
Product();
new
Thread(
new
Runnable(){
@Override
public
void
run() {
for
(
int
i
= 0;
i
< 10;
i
++) {
p
.produce();
}
}
}).start();
new
Thread(
new
Runnable(){
@Override
public
void
run() {
for
(
int
i
= 0;
i
< 10;
i
++) {
p
.consume();
}
}
}).start();
}
|