什么是线程?
在说线程之前,先说说进程。那么什么是进程?进程是程序运行的环境,是操作系统分配资源的最小单元,每个进程都是独立的,其中包含的资源有CPU、内存空间、磁盘IO。而线程就是在一个进程内部的,共享当前进程的所有资源,是CPU调度和分配的最小基本单元。
了解了线程和进程的含义之后,我们在电脑上运行程序,就是启动一个进程,那为什么我们可以同时启动很多程序而系统不卡呢?原因在于CPU时间片轮转机制,CPU为每个进程分配了时间片,如果某个进程在时间片结束时还在运行,那么CPU将被剥夺并分配给另一个进程;如果某个进程在时间片结束前阻塞或者结束,那么CPU就会立即切换。CPU的时间片轮转机制以及上下文切换,在用户没有感知的情况下完成了看上去是同时运行的程序。
什么是并行和并发?
并行:假如有两个饮水机,两个人同时接水,并行是同时做一件事
并发:假如有一个饮水机,两个人交替接水,并发是交替去做,并发不能脱离时间,不然没有意义,一般是在一定时间内研究并发
多线程程序的优缺点
优点
1.可以充分利用操作系统资源
以前是一个线程执行,使用多线程后,可以提高CPU利用率,减少CPU空闲时间,提高并发量
2.加快响应用户的时间
以web项目为例,多开一个子域名,浏览器就多开一个线程去执行
3.可以使代码模块化、异步化、简单化
可以将相同功能代码模块化,将不是实时功能代码异步化,代码结构层次清晰明了
缺点
1.线程之间的安全问题
线程之间是共享进程所有资源的,所以在共享变量的操作有线程安全问题
2.线程之间的死锁问题
A线程持有a的锁,等待b的锁;B线程持有b的锁,等待a的锁,造成循环等待,形成死锁。解决这一类问题,可以考虑在A线程持有a锁,在一定时间内如果一直获取不到b的
锁,就主动释放a的锁。死锁还有很多种,这只是其中一种,具体的可参考百度百科[https://zhidao.baidu.com/question/1448029152492656860.html]
3.线程过多导致的当机问题
线程创建过多,不及时回收,容易造成资源浪费以及可能引发当机问题,合理创建线程,如果可以,多使用线程池技术
下面来说说JAVA线程
JAVA是支持多线程的语言,创建线程的方式有两种,一种是继承Thread类 ,X extend Thread;一种是实现Runnable接口,X implement Runnable。Thread是JAVA针对线程的抽象,Runnable是JAVA针对任务的抽象,这两个类都有一个run()方法,重写这个方法来实现业务逻辑。
JAVA线程运行图 :
线程.png依据上图,我们可以看到,线程新建的时候,只是新建,当调用start()方法的时候,线程进入就绪状态,不是运行状态。在就绪状态,调用join()方法,可以将别的线程加入,让线程按照顺序执行。在运行期,调用sleep()方法,线程进入阻塞状态;sleep()时间到期,线程重新进入就绪状态;调用wait()方法,线程进入阻塞状态,但是这时候要唤醒线程需要调用 notify()、notifyAll()方法才可以。调用yield()方法,线程会让出CPU控制器,重新进入就绪状态。在阻塞状态,如果调用interrupt()方法,线程也会进入就绪状态。运行期间,如果run()方法执行完成,或者调用stop()方法(不推荐使用),setDemon()方法,会让线程进入结束状态。
yield()方法
线程出让自己的CPU时间片给操作系统,和其他线程一起去争夺CPU的时间片,但是不释放自己持有的资源。
setDemon()方法
设置成守护线程,守护线程是一种支持性线程,用于程序中后台调度与支持性工作,例如gc;守护线程与用户线程不同,守护线程在用户线程结束之时,立即结束,具体要看CPU分配的时间片。一般业务代码使用不到守护线程
stop()、interrupt()方法区别:
stop()是强制关闭线程,不释放线程内部持有的资源,例如锁,有引发死锁风险
interrupt()是给某一线程添加一个中断标志位,该线程不一定理会,其可以通过isInterrupt()方法来响应。
线程校验中断调用方法有两个:
isInterrupt()
Thread.interrupted()
其中,第二个方法会在校验中断标志位之后,清除标志位,置为false
最好不要自己实现interrupt()方法,因为如果调用阻塞方法sleep(),wait()的时候,线程是不会检测状态的,但是阻塞方法内部都抛出InterruptedException(),其对中断标志位是有响应的;sleep()方法在抛出InterruptedException之后,会将中断标志位置为false;因为如果不改变中断标志位,那么在阻塞之后,线程的资源得不到释放就被中断了,容易导致线程安全问题。
线程启动的start()方法,和重写的run()方法有何区别?
调用start()方法,将当前线程与操作系统进程挂钩。只能调用一次,如果第二次还调用,则会抛出异常;run()方法只是一个普通方法,与线程无关,只是线程业务逻辑的实现而已,本身可以被任意调用;
synchronized锁
synchronized是java关键字,当它修饰方法时,锁定的是调用这个方法的对象,修饰静态方法或者类时,锁定的是Class对象,所以synchronized锁定的是对象,任何非对象的数据,都不能使用synchronized锁,否则没有锁的效果;
volatile关键字
volatile可以保证共享变量的可见性,一被修改,其他线程可以立即看到。虽然同样进程里面的线程是共享所有资源的,但是JAVA里面还是进行了处理,所以如果需要可见性,就要使用volatile关键字。
ThreadLocal
ThreadLocal.pngThreadLocal包含一个ThreadLocalMap内部类,而每个线程里面都存储了一个ThreadLocal.ThreadLocalMap属性,ThreadLocalMap里面包含一个Entry内部类,Entry实现了WeakReference<ThreadLocal<?>>,而我们使用ThreadLocal时候,经常使用的是ThreadLocal.get(),set(T value),remove()方法;
下面看看ThreadLocal.get()方法源码
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
1:拿到当前线程;
2:从线程中获取ThreadLocalMap;
3:在ThreadLocalMap的Entry对象里面获取数据;
再看看ThreadLocal.set()方法源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
前两步和上面相同;
3:如果当前ThreadLocalMap存在,则替换
4:不存在就创建
使用ThreadLocal的好处是ThreadLocal是线程本地变量,每个线程独一份,不会受到多线程的影响,实现物理隔离,但是不保证数据一致性(也就是说不能使用ThreadLocal来操作共享变量,否则线程不安全)
强引用:Object o = new Object()
,我们new出来的对象o,就是强引用。
软引用:当java虚拟机内存不足引发gc时,软引用会被打上标记并被回收,如果内存充足,则不会;而强引用就算内存不足引发gc时,也不会被回收,除非将强引用置为null(栈里面没有指向堆的引用的时候),或者等程序自动结束。
弱引用:当发生gc时,弱引用一定会被回收。
虚引用:最弱的引用,可以用来做为对象是否存活的监控。
坏处是 可能引发内存泄露,ThreadLocalMap存的是Entry,Entry实现了一个弱引用ThreadLocal类。如果Entry实现强引用的ThreadLocal类,则ThreadLocal百分之百引发内存泄露,因为Entry持有的不仅仅是ThreadLocal对象,还持有Object 对象,所以内存泄露是肯定的。但是为什么说可能呢?因为ThreadLocal.get(),set()方法里面都有内存回收机制,但不是每次都执行,所以可能导致内存泄露,所以我们在使用完ThreadLocal之后,一定要用ThreadLocal.remove()方法清除value值,防止可能引起的内存泄露。
线程之间的协作
等待和通知
wait() notify() notifyAll()
这里有个编程范式:
synchronized(obj){
while(条件不满足){
obj.wait();
}
//TODO
}
synchronized(obj){
obj.notify();
//obj.notifyAll();
}
这里有个要点:wait()方法执行的时候,会自动释放锁资源;
notify(),notifyAll()是当synchronized方法执行完成,才会释放锁资源
等待超时模式实现一个线程池
//线程池
LinkedList<Object> pool = new LinkedList<Object>();
//获取资源
public Object getObj(long mills) throws InterruptedException{
synchronized(pool){
//永不超时
if(mills <=0){
while(pool.isEmpty()){
pool.wait();
}
return pool.removeFirst();
}else{
long future = System.currentTimeMillis() + mills;
long remain = mills;
while(pool.isEmpty() && remain > 0){
pool.wait(remain);
//这里需要计算时间,防止超时(因为线程可能还是没有抢到资源)
remain = future - System.currentTimeMillis();
}
Object obj = null;
if(!pool.isEmpty()){
obj = pool.removeFirst();
}
//可能返回的是null
return obj;
}
}
}
//释放资源
public void releaseObj(Object obj) throws InterruptedException{
if(obj != null){
synchronized(pool){
pool.addLast(obj);
pool.notifyAll();
}
}
}
上面的代码中使用了 wait()范式,notifyAll()方法;
下面来介绍一下wait(),notifyAll(),notify(),sleep(),yield()方法对锁的影响
wait:线程在执行到wait方法的时候,主动释放锁资源,直到被notify,notifyAll唤醒之后,先去抢锁资源,然后执行wait方法后面的内容;
notify、notifyAll:线程执行到这两个方法时,不会释放锁资源,直到synchronized包含的内容走完,才会释放锁资源,所以建议将这两个方法写在synchronized内容的最后面;
sleep:线程进入阻塞状态,不会释放锁资源
yield:线程主动出让CPU控制权,然后和其他线程一起去争抢CPU的时间片,不会释放其持有的资源,包括锁。
网友评论