java|java多线程学习万字长文总结

程序、进程和线程
1.程序是数据加代码加文档的一个静态概念;
2.当程序加载进内存开始运行(所以进程时资源分配的最小单位),整个运行的动态过程称为进程,它是一个动态概念;
3.一个进程里有若干个线程(例如一个java类中会有一个main方法主线程,main方法中还有其他的实例对象进行其相关操作,这些都可以称为main方法主线程的子线程),同一进程内的所有线程共享进程资源。在多线程中每个同级线程的调用是随机的,完全看cpu的心情,所以线程是资源调度的最小单位。当所有线程执行完毕,进程执行完毕。切换线程的代价远小于切换进程,因为同一进程内所有线程共享进程资源,切换时需要保存的信息很少,而切换进程相当于变换了一个资源环境。
线程在程序中的运行状态图
线程在程序运行时的生命周期图(很重要),其中start()方法是使线程进入就绪状态而不是立即执行,所以可以解释为什么线程执行顺序和代码中start的顺序不一定一致,哪个线程先运行完全是看cpu调度的。sleep和wait方法之前的区别很重要,sleep不会释放当前线程持有的对象锁而wait会释放当前线程持有的对象锁。
java|java多线程学习万字长文总结
文章图片

java中多线程的实现方法(创建线程的三种方式)
【java|java多线程学习万字长文总结】首先明确一点:不论是哪种方法实现多线程,只要cpu是单核的话,那么这种多线程都是一种假性多线程,因为单核cpu一次只能处理一个线程,只是因为线程切换的速度(时间片太小)太快,看起来向所有进程同时进行一样,这就是并发的概念,看起来同一时刻一起运行,本质上时间片轮转很快,目的是充分利用cpu资源。而并行才是真正的多个线程一起执行,此时每个线程对应cpu的一个核。
首先明确一点,java其实并没有创建线程的权限,在java源码中创建线程的最底层代码是native本地方法,使用c和c++写的,由他们创建本地物理线程和java创建的线程对象进行映射。
继承Thread类 Thread是java的线程类,其实现了Runnable接口,我们可以使用继承线程类Thread并且重写Thread的run方法来自定义一个线程。我们根据我们想要这个线程做什么来自定以这个线程类,他的具体任务体现在对Thread的run方法的实现逻辑中。当我们需要做这个任务的时候便创建一个自定义线程类分配给他一个线程让他进行相关操作。
示例代码:创建一个下载图片的线程类来实现多线程下载图片。

//继承Thread类 public class testThread extends Thread{ private String url; private String filename; //构造器 public testThread(String url,String filename){ this.url = url; this.filename = filename; } //重写Thread的run方法 @Override public void run(){ downloader loader = new downloader(); loader.download(this.url,this.filename); } public static void main(String[] args){ testThread thread1 = new testThread("url1","filename1"); testThread thread2 = new testThread("url2","filename2"); testThread thread3 = new testThread("url3","filename3"); //三个线程同时开始下载图片,但是url1和url2和url3下载的次序是随机不定的,看cpu先调度哪个 thread1.start(); thread2.start(); thread3.start(); } }//定义了一个下载器类,用于下载图片 class downloader{ @Override void download(String url,String filename) throws Exception{ FileUtils.copyURLToFile(new URL(url),new File(filename)); } }

实现Runnable接口 我们可以通过实现runnable接口并实现run方法来创建自定义的线程类。
示例:龟兔赛跑
public class testRunnable implements Runnable{ //静态变量 记录winner private static String winner; private boolean flag; @Override public void run() { for(int i = 1; i<=100; i++){ //标记比赛是否已经结束 结束则退出循环 flag = isGameOver(i); if(flag) break; //如果当前是兔子线程则睡眠1ms if(Thread.currentThread().getName().equals("兔子")){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"跑了"+i+"步"); } }public static boolean isGameOver(int step){ //所有线程公用winner,当winner出现,所有线程循环结束 if(winner!=null) return true; else { //当前线程跑完100步 设置winner为当前线程名 比赛结束 if(step>=100){ winner = Thread.currentThread().getName(); System.out.println("winner is "+winner ); return true; } } //比赛没结束 return false; }public static void main(String[] args) { testRunnable test = new testRunnable(); new Thread(test,"兔子").start(); new Thread(test,"乌龟").start(); } }

实现Callable接口 实现Callable接口的线程类需要使用服务提交方式运行,并且运行方法call有返回值。
实现Callable接口来创建线程的步骤:
1.实现callable接口,需要返回值类型
2.重写run方法,需要抛出异常
3.创建目标对象
4.创建执行服务 ExecutorService ser = Executors.newFixedThreadPool(1);
5.提交执行 Future result1 = ser.submit(t1);
6.获取执行结果 boolean r1 = result1.get();
7.关闭服务 ser.shutdownNow();
示例:多线程下载图片
//实现Callable接口 public class testCallable implements Callable{ private String url; private String filename; //构造器 public testCallable(String url,String filename){ this.url = url; this.filename = filename; } //重写Callable的call方法 @Override public Boolean call(){ downloader loader = new downloader(); loader.download(this.url,this.filename); return true; } public static void main(String[] args){ testCallable thread1 = new testCallable("url1","filename1"); testCallable thread2 = new testCallable("url2","filename2"); testCallable thread3 = new testCallable("url3","filename3"); //创建服务 ExecutorService ser = Executors.newFixedThreadPool(1); //提交线程并执行 Future r1 = ser.submit(thread1); Future r2 = ser.submit(thread2); Future r3 = ser.submit(thread3); //获取执行结果 输出三个true System.out.println(r1.get()); System.out.println(r2.get()); System.out.println(r3.get()); //关闭服务 ser.shutdownNow(); } }//定义了一个下载器类,用于下载图片 class downloader{ @Override void download(String url,String filename) throws Exception{ FileUtils.copyURLToFile(new URL(url),new File(filename)); } }

线程方法
线程停止和线程休眠 java提供了线程停止的api为Thread.interrupt()方法,但不建议使用。我们完全可以采取标志信号的方式告诉线程停止运行,例如下面示例:
线程休眠为Thread.sleep()方法,参数单位是毫秒,效果是使当前线程沉睡若干毫秒。
public class testStop extends Thread{//定义一个线程停止的符号位 boolean flag = true; @Override public void run() { while(flag){ System.out.println("线程在进行"); } }//线程停止方法 public void end(){ this.flag = false; }public static void main(String[] args) throws InterruptedException { testStop testStop = new testStop(); testStop.start(); //让主线程睡0.1秒后再停止testStop线程 Thread.sleep(100); testStop.end(); } }

线程礼让 线程礼让的方法是thread.yield()但是该方法只是将当前进程退出cpu重新进入就绪状态和其他就绪的进程再次重新抢占cpu,所以有可能礼让之后还是原线程继续执行。
public class testYield extends Thread{ //定义当前线程名 private String name; public testYield (String name){ this.name = name; }@Override public void run() { System.out.println(this.name+" start!"); //线程礼让 this.yield(); //try { //this.sleep(3000); //} catch (InterruptedException e) { //e.printStackTrace(); //} System.out.println(this.name+" end!"); }public static void main(String[] args) { testYield a = new testYield("a"); testYield b = new testYield("b"); a.start(); b.start(); } }//输出是 b start! a start! b end! a end!

线程强制执行,抢占cpu 使用thread.join()方法强制执行进程(插队行为)
public class testJoin {public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new myJoin()); //程序启动主线程开始跑 System.out.println("当前线程:"+ Thread.currentThread().getName()); //此时 thread 就绪但是cpu被main抢占无法使用 thread.start(); for (int i = 0; i < 10; i++) { if (i==5){ // 进程thread插队强制执行 thread.join(); } System.out.println("当前线程:"+ Thread.currentThread().getName()); }}}class myJoin implements Runnable{@Override public void run() { System.out.println("我插个队"); System.out.println("当前线程:"+ Thread.currentThread().getName()); try { //插队睡一秒觉 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我ok了"); }}输出 当前线程:main 当前线程:main 当前线程:main 当前线程:main 当前线程:main 当前线程:main 我插个队 当前线程:Thread-0 我ok了 当前线程:main 当前线程:main 当前线程:main 当前线程:main 当前线程:main

正常来说 main线程先执行 且不需要等待子线程完成主线程就可以结束 但是如果main线程被子线程插队就要等子线程结束main线程才能结束。此外,线程的run方法和start方法的区别在于调用线程的run方法需要等线程的run方法执行完毕后main线程才会继续执行,而调用线程的start方法才是真正的多线程并行运行。
public class testJoin {public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new myJoin()); System.out.println("当前线程:"+ Thread.currentThread().getName()); thread.start(); System.out.println(Thread.currentThread().getName()+"结束"); }}class myJoin implements Runnable{@Override public void run() { System.out.println("我插个队"); System.out.println("当前线程:"+ Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我ok了"); }}输出 当前线程:main main结束 我插个队 当前线程:Thread-0 我ok了

获取线程状态 Thread类中有一个State属性,该属性是一个枚举类,Thread.State枚举类一共有6种枚举值,可以通过Thread.getState()方法来获取线程的当前状态。
java|java多线程学习万字长文总结
文章图片

线程优先级 Thread.priority属性代表当前线程的调度优先级,可以通过Thread.getPriority()方法来获取线程的调度优先级,也可以使用Thread.setPriority()方法来设置线程优先级,线程优先级的默认大小为5,最小优先级为1,最大为10,两者都是不可变属性。cpu往往先调用优先级高的线程。
查看Thread源码可见,如果我们设定的优先级priority的值如果大于10或小于1,会抛出非法参数异常。
java|java多线程学习万字长文总结
文章图片

java|java多线程学习万字长文总结
文章图片

守护线程 线程可以分为两种,一种是用户线程,一种是守护线程(后台线程),JVM必须等待用户线程运行结束才能退出,但是JVM不需要等待守护线程结束,最常见的守护线程有GC(垃圾回收)、记录操作日志、监控内存使用等后台线程。
public class testDaemon {public static void main(String[] args) { DaemonThread d = new DaemonThread(); UserThread u = new UserThread(); //分别创建一个用户线程和一个守护线程 Thread daemon = new Thread(d); //设置daemon为守护线程,Daemon属性默认为false daemon.setDaemon(true); //守护线程和用户线程开始运行 daemon.start(); new Thread(u).start(); } }class DaemonThread implements Runnable{ @Override public void run() { //后台程序一直运行 while(true){ System.out.println("我一直在后台守护"); } } }class UserThread implements Runnable{ @Override public void run() { //10次循环后用户线程结束 for (int i = 0; i < 10; i++) { System.out.println("用户线程正在进行"+i); } } } //看到虽然守护线程执行条件是while(true) 但是JVM仍然结束退出了 输出: 我一直在后台守护 我一直在后台守护 我一直在后台守护 用户线程正在进行4 用户线程正在进行5 用户线程正在进行6 用户线程正在进行7 用户线程正在进行8 用户线程正在进行9 我一直在后台守护 我一直在后台守护 我一直在后台守护Process finished with exit code 0

线程同步机制
当多个线程操作同一个对象时,会将堆中的数据复制一份副本到自己的操作空间来进行相关操作,操作完之后回传修改堆中的数据。此时可能会引发数据一致性问题,当两个线程同时取到同一时刻的数据副本时,后一个结束的线程的操作会覆盖上一个线程对该数据的操作,例如我们在使用arraylist的时候由于arraylist是线程不安全的类,a线程添加的值有可能会被b线程添加的值覆盖。在一些特定场景下,线程不同步会导致更严重的问题,例如卡里只有100元,两人同时取100元,最后卡里剩-100元。
java|java多线程学习万字长文总结
文章图片

一个线程不安全的案例
public class testThreadUnSafe { public static void main(String[] args) { account account = new account(100); //线程a b同时取同一个account对象 drawing a = new drawing(account,50,"a"); drawing b = new drawing(account,100,"b"); a.start(); b.start(); }}class account{ int money; public account(int money){ this.money = money; } }class drawing extends Thread{account account; int drawMoney; public drawing(account account,int drawMoney,String name) { super(name); this.account = account; this.drawMoney = drawMoney; }@Override public void run() { //余额不够无法取 if (drawMoney>account.money){ System.out.println("余额不足取不了,拒绝"+Thread.currentThread().getName()); } else{ //a等待b一下,否则a执行过快无法测出效果 确保a和b拿到了同样副本 if(Thread.currentThread().getName() == "a") { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //取钱 计算余额 account.money-= drawMoney; System.out.println(Thread.currentThread().getName()+"取了"+drawMoney+",余额="+account.money); } } }输出b取了100,余额=0 a取了50,余额=-50Process finished with exit code 0

看到此时卡里余额-50,卡里只有100元却取出了150元,这就是线程不同步造成的安全问题。
解决线程同步问题 首先要确保多个线程要按一定顺序来操作同一个数据对象,同时在进行写入操作的时候对数据对象加锁,此时其他线程无法操作该对象,并且也无法对该对象继续加锁,这样就能确保一次只有一个线程修改该对象。java多线程使用synchronize方法和synchronize代码块来实现线程同步,需要注意的是synchronize方法是对方法调用者对象加锁,而synchronize代码块可以给指定对象加锁。我们可以将上述不安全案例改成安全的。
public void run() { //只需要将操作都放到synchronized代码块中就可以实现同步,此时对account对象上锁,这时别的线程无法访问account synchronized (account){ //余额不够无法取 if (drawMoney>account.money){ System.out.println("余额不足取不了,拒绝"+Thread.currentThread().getName()); } else{ //a等待b一下,否则a执行过快无法测出效果 确保a和b拿到了同样副本 if(Thread.currentThread().getName() == "a") { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //取钱 计算余额 account.money-= drawMoney; System.out.println(Thread.currentThread().getName()+"取了"+drawMoney+",余额="+account.money); } } } 输出 a取了50,余额=50 余额不足取不了,拒绝bProcess finished with exit code 0

如果对run方法加synchronized,使其变成同步方法锁定的是drawing对象而不是account对象,其他线程还是可以操作account对象
//声明run方法为同步方法,此时锁定的是drawing对象 无效 public synchronized void run() { //余额不够无法取 if (drawMoney>account.money){ System.out.println("余额不足取不了,拒绝"+Thread.currentThread().getName()); } else{ //a等待b一下,否则a执行过快无法测出效果 确保a和b拿到了同样副本 if(Thread.currentThread().getName() == "a") { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //取钱 计算余额 account.money-= drawMoney; System.out.println(Thread.currentThread().getName()+"取了"+drawMoney+",余额="+account.money); } } 输出 b取了100,余额=0 a取了50,余额=-50Process finished with exit code 0

使用锁来保证线程安全 定义一个锁对象也可以实现上述synchronized一样的效果
public static void main(String[] args) { account account = new account(100); //定义锁 ReentrantLock lock = new ReentrantLock(); //线程a b同时取同一个account对象 但此时两个对象使用同一个锁对象 drawing a = new drawing(account,50,"a",lock); drawing b = new drawing(account,100,"b",lock); a.start(); b.start(); }//线程的run方法执行前后加锁 释放锁 public void run() { //对代码块加锁 lock.lock(); //余额不够无法取 if (drawMoney>account.money){ System.out.println("余额不足取不了,拒绝"+Thread.currentThread().getName()); } else{ //a等待b一下,否则a执行过快无法测出效果 确保a和b拿到了同样副本 if(Thread.currentThread().getName() == "a") { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //取钱 计算余额 account.money-= drawMoney; System.out.println(Thread.currentThread().getName()+"取了"+drawMoney+",余额="+account.money); } //释放锁 lock.unlock(); } 输出(此时数据安全) a取了50,余额=50 余额不足取不了,拒绝bProcess finished with exit code 0

死锁
死锁产生的四个必要条件有:1.互斥访问 2.在请求其他资源阻塞时不释放现有资源 3.不可以抢占资源 4.环路等待
在写程序时要避免synchronized嵌套synchronized的情况,此情况下容银引发死锁问题。例子如下:
两个线程完成执行分别需要资源a和资源b 然而两个线程分别占有一个资源,互相等待对方的资源而不释放占有的资源 从而导致死锁
public class testDeadLock { public static void main(String[] args) { A a = new A(); B b = new B(); //a b 资源两个线程互斥共用 thread_1 t1 = new thread_1("线程1",a,b); thread_2 t2 = new thread_2("线程2",a,b); t1.start(); t2.start(); } }class A{ //资源a被占用 public void occupy(){ System.out.println(Thread.currentThread().getName()+"已经占有a,还想占有b"); } }class B{ //资源b被占用 public void occupy(){ System.out.println(Thread.currentThread().getName()+"已经占有b,还想占有a"); } }class thread_1 extends Thread{ //线程完成执行需要的资源 a b A a; B b; public thread_1(String name,A a,B b){ super(name); this.a = a; this.b = b; } @Override public void run() { //synchronized嵌套会出现死锁问题 线程1给资源a上锁别的线程无法访问a 线程2拿不到a锁 synchronized (a){ a.occupy(); synchronized (b){ b.occupy(); } } } }class thread_2 extends Thread{ //线程完成执行需要的资源 a b A a; B b; public thread_2(String name,A a,B b){ super(name); this.a = a; this.b = b; } @Override public void run() { //synchronized嵌套会出现死锁问题 线程2给资源b上锁别的线程无法访问b 线程1拿不到b锁 synchronized (b){ b.occupy(); synchronized (a){ a.occupy(); } } } }输出 线程1已经占有a,还想占有b 线程2已经占有b,还想占有a程序死锁 一直无法结束退出

线程通信
使用对象的notify和wait方法来唤醒等待该对象资源的线程或阻塞当前线程并释放对象锁。wait和sleep的区别:1.wait会释放对象锁,sleep不会 2.wait是对象实例调用,sleep是线程类调用。wait和notify只是告诉该资源等待队列中的线程我释放了这个资源的控制权,其他线程并不会立刻执行,只有一个能获取该资源然后等待cpu调度。
示例:消费者,生产者模型
wait和notify要写在synchronized代码块内来控制线程间通信
wait:使当前前程阻塞,并释放持有的锁,并加入该锁的资源等待队列
notify:随机唤醒该资源等待队列的一个线程,但没有让出锁,此时被唤醒的线程不会执行
public class ConsumerAndProducer {public static void main(String[] args) { //生产者 消费者 互斥共用同一个货架 Queue goods = new LinkedList<>(); Producer p = new Producer(goods); Consumer c = new Consumer(goods); p.start(); c.start(); } }class Good{ String name; public Good(String name){ this.name = name; } }class Consumer extends Thread{ //商品队列 Queue goods; //记录当前商品号 int i = 1; public Consumer(Queue goods){ this.goods = goods; }@Override public void run() { while (i <= 30) { synchronized (goods) { while (goods.size() > 0) { consume(); //只是唤醒该资源wait队列中的进程 但没有让出锁 goods.notify(); } if (goods.size() == 0) { System.out.println("没货了,请求生产者补货"); try { //如果货架上品没有了 则让出锁给生产者来补货 如果已经生产了30件货则退出程序 if(i!=31) goods.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }public void consume(){ Good good = goods.poll(); System.out.println("消费者消费了商品"+good.name); i++; } }class Producer extends Thread{//商品队列 Queue goods; //最大商品数 int max_size = 10; //记录当前商品号 int i =1; public Producer(Queue goods){ this.goods = goods; }@Override public void run() { while(i<=30){ synchronized (goods){ while (goods.size() < max_size) { product(); //唤醒wait队列中的消费者进程 告知已经补了货 但没有让出锁 goods.notify(); } if (goods.size() == max_size) { System.out.println("货满了,等待消费者消费"); try { //货架的货已补满 让出锁给消费者消费 goods.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }public void product(){ Good good = new Good("商品"+i); i++; goods.add(good); System.out.println("生产者了商品"+good.name); } } 输出 生产者了商品商品1 生产者了商品商品2 生产者了商品商品3 生产者了商品商品4 生产者了商品商品5 生产者了商品商品6 生产者了商品商品7 生产者了商品商品8 生产者了商品商品9 生产者了商品商品10 货满了,等待消费者消费 消费者消费了商品商品1 消费者消费了商品商品2 消费者消费了商品商品3 消费者消费了商品商品4 消费者消费了商品商品5 消费者消费了商品商品6 消费者消费了商品商品7 消费者消费了商品商品8 消费者消费了商品商品9 消费者消费了商品商品10 没货了,请求生产者补货 生产者了商品商品11 生产者了商品商品12 生产者了商品商品13 生产者了商品商品14 生产者了商品商品15 生产者了商品商品16 生产者了商品商品17 生产者了商品商品18 生产者了商品商品19 生产者了商品商品20 货满了,等待消费者消费 消费者消费了商品商品11 消费者消费了商品商品12 消费者消费了商品商品13 消费者消费了商品商品14 消费者消费了商品商品15 消费者消费了商品商品16 消费者消费了商品商品17 消费者消费了商品商品18 消费者消费了商品商品19 消费者消费了商品商品20 没货了,请求生产者补货 生产者了商品商品21 生产者了商品商品22 生产者了商品商品23 生产者了商品商品24 生产者了商品商品25 生产者了商品商品26 生产者了商品商品27 生产者了商品商品28 生产者了商品商品29 生产者了商品商品30 货满了,等待消费者消费 消费者消费了商品商品21 消费者消费了商品商品22 消费者消费了商品商品23 消费者消费了商品商品24 消费者消费了商品商品25 消费者消费了商品商品26 消费者消费了商品商品27 消费者消费了商品商品28 消费者消费了商品商品29 消费者消费了商品商品30 没货了,请求生产者补货Process finished with exit code 0

线程池
线程的创建和销毁是有开销的,如果每进一个线程就创建一个,没出一个就销毁一个,会造成极大的开销。
所以引入线程池就是实现创建好多个线程在线程池中,把线程池放在jvm中,使用时直接获取,使用完也不会销毁,避免重复的线程创建(new)和销毁(destroy),实现重复利用,加快程序响应速度和提高资源利用率,同时方便线程管理。
线程池的详细内容不过多介绍,详情请看juc编程的学习章节。

    推荐阅读