多线程
并行和并发
这里的时间都是微观上的概念
- 并行:指两个或多个事件在同一时刻发生,强调的是时间点的瞬间
- 并发:指两个或多个事件在一个时间段内(时间很短,如1纳秒内)先后发生,强调的是时间段
当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度,这种情况下,线程是一个接一个执行的。
进程和线程
-
进程:指一个内存中运行中的应用程序.
有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
-
线程:指进程中的一个执行任务(控制单元),一个进程可以同时并发运行多个线程.
堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间可以影响的,又称为轻型进程或进程元。
进程中的多个线程是并发执行的,从微观上来讲,线程执行是有先后顺序的,那么哪个线程执行完全取决于CPU调度器,程序员是控制不了的。
我们可以把多线程并发性看作是多个线程在瞬间抢CPU资源,谁抢到资源谁就运行,这也造就了多线程的随机性。
Java程序的进程里至少包含主线程和垃圾回收线程(后台线程)。
多线程的优势
多线程作为一种多任务、并发的工作方式,当然有其存在优势:
- 提高应用程序响应:
这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作置于一个新的线程,可以避免这种尴尬的情况。
- 使多CPU系统更加有效:
操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
- 改善程序结构:
一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
Java操作进程
Runtime:
- 使java应用程序与其运行环境相连接,可以通过getRuntime()获取当前运行时.
- 通过exec(String command)方法 执行应用程序.
多线程的创建
继承Thread类:
- 自定义类继承于Thread类,那么该自定义类就是线程类;
- 覆写run方法,将线程运行的代码存放在run中;
- 创建自定义类的对象,即线程对象;
- 调用线程对象的start方法,启动线程。
实现Runnable接口:
- 自定义类实现Runnable接口;
- 覆写run方法,线程运行的代码存放在run中;
- 通过Thread类创建线程对象,并将实现了Runnable接口的实现类对象作为参数传递给Thread类的构造器。
- Thread类对象调用start方法,启动线程。
继承和实现的对比:
- 都能完成同样的功能
- 使用继承的方式比较方便
- Java中继承的机制的单继承,即继承Thread后,不能再继承其他的类,但是Java对于接口是多实现的,实现Runnable后依然可以实现其他接口
-
从面向对象的角度来看,把线程的任务代码单独的放在Runnable接口中,以参数的形式传递Runable对象到Thread中,实现线程的任务代码和线程对象分离,保留了继承,更符合面向对象的思想
推荐使用实现Runnable接口的方式来创建线程
线程并发的安全问题
多个线程访问共享区域(堆),产生了数据不安全的问题(不同步).
场景:
当线程A只执行了变量值的修改,还未打印变量的值,此时线程的执行权被其他的线程B所抢走,别的线程又修改了变量的值,并打印了变量值,当原来的线程A抢回执行权后变量的值已经被别的线程B所修改,线程A打印的值就与线程B打印的值相同, 所以就有了数据重复的问题
解决安全问题的方法:
-
同步代码块
synchronized(同步监听对象) { .... }
同步监听对象,只要是一个对象即可,需要有同步的效果,该对象就必须是唯一对象
凡是操作了共享数据的代码必须同步起来
同步监听对象就像一把锁一样,一个线程进去代码块后就会使用锁把门给锁上,别的线程就进不去了,只能在门口等着,直到代码块里的线程执行完毕,把锁释放之后,别的线程才能进去,至于是哪个线程进去,由CPU的调度决定.
-
同步方法
public synchronized void m(){ ... }
和同步代码块是一样的,仅仅是形式不同而已
静态方法的同步监听对象是当前类的字节码对象 类名.class
实例方法的同步监听对象是:this
懒汉式的单例模式:
private static $CLASS_NAME$ instance ;
private $CLASS_NAME$($param1$){
$init$
}
public static $CLASS_NAME$ getInstance($param1$){
//如果有多个线程进来后,1个线程进入后,其他线程被堵在外面,效率比较差,增加一个null判断.
if (instance == null) {
synchronized ($CLASS_NAME$.class){
if (instance == null) {
instance = new $CLASS_NAME$($param2$) ;
}
}
}
return instance ;
}
-
锁机制
Lock接口
可以自由的控制什么时候上锁,什么时候释放锁 lock();//获取锁 unlock();//释放锁
ReentrantLock
一个可重入的互斥锁,功能比sychronized更强大
示例代码:
Lock lock = new ReentrantLock(); public void run(){ while(true){ lock.lock();//执行操作共享数据前上锁 try{ .....//执行数据操作 }catch(Exception e){ }finally{ lock.unlock();//执行完毕释放锁 } } }
生产消费者案例
由生产者(producer)往共享空间存放数据,再由消费者(consumer )取出数据
共享数据
public void add(String name, String taste) {
try {
this.name = name;
System.out.println("做好了菜:"+name);
Thread.sleep(5);
this.taste = taste;
System.out.println("调味后生产了一道菜:" + name + " 口味:" + taste);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void take() {
try {
Thread.sleep(5);
System.out.println("消费了一道菜:" + name + " 口味:" + taste);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
生产消费者线程
public void main(){
//共享的数据区域
ShareData data = new ShareData();
//生产者线程
new Thread(new Producer(data)).start();
//消费者线程
new Thread(new Consumer(data)).start();
}
public class Producer implements Runnable{
private ShareData mShareData;
public Producer(ShareData shareData) {
mShareData = shareData;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
mShareData.add("麻辣小龙虾","麻辣");
}else{
mShareData.add("清蒸排骨","清淡");
}
}
}
}
public class Consumer implements Runnable{
private ShareData mShareData;
public Consumer(ShareData shareData) {
mShareData = shareData;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
mShareData.take();
}
}
}
出现问题:
做好了菜:麻辣小龙虾
调味后生产了一道菜:麻辣小龙虾 口味:麻辣
做好了菜:清蒸排骨
调味后生产了一道菜:清蒸排骨 口味:清淡
做好了菜:麻辣小龙虾
消费了一道菜:麻辣小龙虾 口味:清淡
调味后生产了一道菜:麻辣小龙虾 口味:麻辣
做好了菜:清蒸排骨
消费了一道菜:清蒸排骨 口味:麻辣
调味后生产了一道菜:清蒸排骨 口味:清淡
做好了菜:麻辣小龙虾
消费了一道菜:麻辣小龙虾 口味:清淡
调味后生产了一道菜:麻辣小龙虾 口味:麻辣
做好了菜:清蒸排骨
消费了一道菜:清蒸排骨 口味:麻辣
调味后生产了一道菜:清蒸排骨 口味:清淡
做好了菜:麻辣小龙虾
消费了一道菜:麻辣小龙虾 口味:清淡
调味后生产了一道菜:麻辣小龙虾 口味:麻辣
做好了菜:清蒸排骨
消费了一道菜:清蒸排骨 口味:麻辣
调味后生产了一道菜:清蒸排骨 口味:清淡
-
先消费,后生产
生产者已经生产好后,正准备打印的时候,CPU调度了消费者的线程,生产者的线程停下来了,先由消费者消费,自然出现了先消费的现象
解决方案:
必须先等生产者生产好后,再由消费者消费
-
多消费,多生产
生产的线程在第一执行时被抢了,先出现先消费的情况后,下一次执行时,比较顺利就会看到生次的情况,同样的道理,对于消费者来讲,就会出现消费两次的情况
解决方案:
必须先等生产者生产好后,再由消费者消费
-
菜和口味不符
在生产者生产的时候,只做了菜名,没有上调料的时候,线程的执行被消费者抢了,
消费者访问到的是新做的菜和上次留下的调料,出现了口味的错乱的问题解决方案:
必须先等生产者生产好后,再由消费者消费,需要加上同步
解决方案一:
1.要保证同步
使用同步的方法
2.要保证生产者先执行
先设置一个标志判断是否有东西,没有就执行生产者,否则执行消费
3.要保证线程中的内容执行完毕,另一个线程才能执行
使用等待唤醒机制(Object)
Object的等待唤醒:
wait(); //使当前线程进入等待, 必须由当前线程的同步监听对象调用
notifyAll();//唤醒在同步对象上等待的所有线程,必须由当前线程的同步监听对象调用
改良后的代码:
public class ShareData {
private String name;
private String taste;
private boolean hasData;
//同步方法
public synchronized void add(String name, String taste) {
try {
if (hasData){
//存在数据,生产者线程进入等待
this.wait();
}
this.name = name;
System.out.println("做好了菜:"+name);
Thread.sleep(5);
this.taste = taste;
System.out.println("调味后生产了一道菜:" + name + " 口味:" + taste);
//生产完毕,改变标记,唤醒消费者线程
hasData = true;
this.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//同步方法
public synchronized void take() {
try {
if (!hasData){
//无数据,消费者线程等待
this.wait();
}
Thread.sleep(5);
System.out.println("消费了一道菜:" + name + " 口味:" + taste);
//消费完毕,改变标记,唤醒生产者线程
hasData = false;
this.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
解决方案二:
使用锁的方式
Condition
-
锁的同步监听对象, Lock代替了Synchronized方法和语句的使用, Condition代替了Object中的同步监听对象.
await() //造成当前线程在接到信号或者被中断之前一直处于等待状态,替代Object的wait();
singalAll() //唤醒所有等待线程, 替代Object的notifyAll();
newCondition() //Lock接口的方法创建Condition对象
示例代码:
public class ShareDataForLock {
private String name;
private String taste;
private boolean hasData;
//必须先创建锁对象
private Lock mLock;
//锁的同步监听对象
private Condition mCondition;
public ShareDataForLock() {
mLock = new ReentrantLock();
mCondition = mLock.newCondition();
}
public void add(String name, String taste) {
mLock.lock();//加锁
try {
if (hasData){
//存在数据,生产者线程进入等待
mCondition.await();
}
this.name = name;
System.out.println("做好了菜:"+name);
Thread.sleep(5);
this.taste = taste;
System.out.println("调味后生产了一道菜:" + name + " 口味:" + taste);
//生产完毕,改变标记,唤醒消费者线程
hasData = true;
mCondition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
mLock.unlock();//释放锁
}
}
//同步方法
public synchronized void take() {
mLock.lock();//加锁
try {
if (!hasData){
//无数据,消费者线程等待
mCondition.await();
}
Thread.sleep(5);
System.out.println("消费了一道菜:" + name + " 口味:" + taste);
//消费完毕,改变标记,唤醒生产者线程
hasData = false;
mCondition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
mLock.unlock();//释放锁
}
}}
睡眠和等待的区别
Thread的sleep方法和Object的wait方法
1:他们都能让线程暂停执行
2:sleep方法在暂停执行的过程中是不会失去同步监听对象
wait方法在暂停执行的过程中会失去同步的监听对象,醒来后会重新拿到同步监听对象,再执行代码
死锁
所谓死锁就是锁死了,由于同步使用不当(一般是由于同步嵌套同步),产生的一种线程代码不执行的现象,JVM不会关闭,死锁一旦产生,无法挽救,只能事先避免,也称为线程的阻塞状态
共享的同步监听对象得不到释放,线程在互相等待资源释放,造成死锁的现象
示例代码:
final String a = "333";
final String b = "555";
new Thread(){
@Override
public void run() {
while (true){
synchronized (a){
System.out.println("thread1:11");
synchronized (b){
System.out.println("thread1:22");
}
}
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true){
synchronized (b){
System.out.println("thread2:1111");
synchronized (a){
System.out.println("thread2:2222");
}
}
}
}
}.start();
执行结果:
I/System.out: thread2:1111
I/System.out: thread2:2222
I/System.out: thread2:1111
I/System.out: thread1:11 //此时a对象被t1占用,t2等待t1释放a,t1等待t2释放b,死锁
要想不发生死锁,建议使用Lock对象来完成同步
线程生命周期
- 1:新建状态(new):使用new创建一个线程对象
-
2:可运行状态(runnable):分成两种状态,ready和running。分别表示就绪状态和运行状态。
start启动线程后,线程处于就绪的状态,要等CPU调用线程时才是运行状态
3:阻塞状态(blocked):正在运行的线程遇到某个特殊情况如,同步、等待I/O操作完成等就会进入阻塞状态
-
4:等待状态(waiting):
当前线程等待另外一个线程执行某个操作,此时当前线程处于等待状态 wait --->notifyAll
-
5:计时等待状态(timed waiting) :
当前线程等待另外一个线程来执行有指定等待时间的的操作,此时当前线程处于计时等待状态 可运行的状态下调用sleep、wait方法,就会进入等待状态 sleep是由线程来调用了,等待时不会失去监听对象 wait是由同步监听对象调用的,等待时会失去监听对象 可以调用notify、notifyAll方法回到可运行状态上,等待的时间到了,也会回到可运行的状态
-
6:终止状态(terminated):通常称为死亡状态,表示线程终止
getState() 返回该线程的状态
联合线程
线程的join()方法表示一个线程等待另一个线程完成后才执行。
有人也把这种方式称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程。
- join()方法被调用之后,线程对象处于等待状态
- join()方法是在线程开启start()后再调用的
- 线程1.join 表示在原来的线程A中插入线程1,需要等线程1执行完毕才继续执行原线程A
后台线程
在后台运行,其目的是为其他线程提供服务,也称为"守护线程"。JVM的垃圾回收器就是典型的后台线程。
特点:
若所有的前台线程都死亡,后台线程自动死亡。
thread.isDaemon() ---> 测试后台线程的方法
thread.setDaemon(true) ---> 设置后台线程的方法 线程创建默认是前台线程,
该方法必须在start()前调用,否则出现IllegalThreadStateException异常.
线程优先级
- 每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关,并非线程优先级越高的就一定先执行,哪个线程的先运行取决于CPU的调度。
-
setPriority() 默认是5,最高是10,最低是1
这个设置一般都不去手动设置,默认即可,因为不是所有系统都认识1-10中的优先级,但是1-5-10这3个优先级所有系统都认识
线程局部
ThreadLocal和线程同步机制
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
- 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
-
ThreadLocal
从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
-
定义
ThreadLocal是一个线程内部数据存储类,通过他可以在指定的线程中存储数据。存储后,只能在指定的线程中获取到存储的数据,对其他线程来说无法获取到数据。
-
使用场景
日常使用场景不多,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,可以考虑使用ThreadLocal。
Android源码的Lopper、ActivityThread以及AMS中都用到了ThreadLocal。
-
示例代码
public class ThreadLocalActivity extends AppCompatActivity { private ThreadLocal<String> name = new ThreadLocal<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread_local); name.set("小明"); Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get()); new Thread("thread1") { @Override public void run() { name.set("小红"); Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get()); } }.start(); new Thread("thread2") { @Override public void run() { Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get()); } }.start(); } } 输出: D/ThreadLocalActivity: Thread:main name:小明 D/ThreadLocalActivity: Thread:thread1 name:小红 D/ThreadLocalActivity: Thread:thread2 name:null 可以看到虽然访问的是同一个ThreadLocal对象,但是获取到的值却是不一样的
-
源码分析结果
根据源码分析得知,每个线程Thread中都有一个ThreadLocalMap类型的threadLocals成员变量来保存数据,通过ThreadLocal类来进行维护。
我们每次在不同线程调用ThreadLocal的set方法set的数据是存在不同线程的ThreadLocalMap中的,就像注释说的ThreadLocal只是起了个维护ThreadLocalMap的功能。想到是get方法同样也是到不同线程的ThreadLocalMap去取数据。
-
ThreadLocal内存泄漏的原因
ThreadLocalMap中存储数据的内部类Entry
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
- Entry这里的key是使用了个弱引用,所以因为使用弱引用这里的key,ThreadLocal会在JVM下次GC回收时候被回收,而造成了个key为null的情况,而外部ThreadLocalMap是没办法通过null key来找到对应value的。如果当前线程一直在运行,那么线程中的ThreadLocalMap也就一直存在,而map中却存在key已经被回收为null对应的Entry和value却一直存在不会被回收,造成内存的泄漏。
设计者使用弱引用是由原因的:
如果使用强引用,那么如果在运行的线程中ThreadLocal对象已经被回收了但是ThreadLocalMap还持有ThreadLocal的强引用,若是没有手动删除,ThreadLocal不会被回收,同样导致内存泄漏。
如果使用弱引用ThreadLocal的对象被回收了,因为ThreadLocalMap持有的是ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。nullkey的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
ThreadLocal的内存内泄漏的真正原因并不能说是因为ThreadLocalMap的key使用了弱引用,而是因为ThreadLocalMap和线程Thread的生命周期一样长,没有手动删除Map的中的key才会导致内存泄漏。所以解决ThreadLocal的内存泄漏问题就要每次使用完ThreadLocal,都要记得调用它的remove()方法来清除。
-
线程池
Executor
定时器
Timer:
一种工具,用其安排在后台线程中执行任务,可安排执行一次,也可以定期重复执行.
Timer timer =new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("1000毫秒后执行了任务");
}
},1000);