说在前面,我自己是不喜欢看特别长篇的博客。我能理解看长篇博客的那种难受,因此,想直接入正题看编码的不妨跳过前两块内容,直接进入“线程的创建”。
线程概述
什么是线程?
之前看到过某大神这样一句话:比较糟糕的技术文档的特征:用专业名词来解释专业名词
从这句话中,我受益匪浅,因此以后的文章尽量都避免这样。相信大家也不喜欢看一些概念性的东西吧。
介绍线程之前,首先得解释一下进程,因为要用进程来解释线程。
进程:是程序的一次动态执行过程。比如打开谷歌浏览器,从它打开的时刻开始就启动了一个进程。
多进程:多进程就像打开了多个程序,比如边玩QQ,边听歌,还可以浏览网页,多个任务同时运行。
线程:比进程更小的执行单位,通常一个进程拥有1-n个线程。
多线程:指在同一程序(进程)中能够同时处理多个任务,而这些任务就对应多个线程,比如浏览器下载图片能够同时下载多张图片。比如ABC三个用户同时访问淘宝网,淘宝网的服务器收到A用户的请求后,为A用户创建了一个线程,BC用户同样拥有自己对应的线程。注意:多线程不是为了提高程序执行速度(性能甚至更低),而是提高应用程序的使用效率。
线程的生命周期
每一个生命周期也是一种状态。
1、新建:创建一个新的线程对象
2、就绪:线程对象创建后,其他线程调用了该对象的start()方法,该线程被放入可运行的线程池中,等待获取CPU的使用权。
(这里可以这样解释,百米赛跑上,每个运动员对应一个线程,裁判员大喝“预备”这声预备相当于调用了start()方法,所有运动员进入就绪状态,开枪这个动作则对应了获取CPU使用权,运动员得到命令开跑)注意:一个CPU核心在某一时刻只能运行一个线程,这里的CPU相当于多核CPU,不扯远了。。。
3、运行:就绪状态的线程获取了CPU,执行。(运动员开跑)
4、阻塞:阻塞情况比较复杂,这里简单考虑一下就不分类了,即线程因为某种原因放弃CPU使用权,线程暂停。(某运动员抢跑,此次比赛暂停,所有运动员回起跑线重新准备就绪,注意不能继续跑!也就是说不能到运行状态)
5、死亡:线程执行完成或异常退出,该线程生命周期结束。
状态转换图如下:
JAVA线程的创建
好了,上面啰嗦了这么多,不过相信大家已经对线程有了一定理解吧,回到也许是大家最想看到的部分,coding...
1、继承Thread类
这里写一个简单的类,主线程输出1-5,两个子线程输出1-10
public class FirstThread extends Thread { public void run() { for (int i = 1; i <= 10; i++) { System.out.println(getName() + " " + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String args[]) { for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } new FirstThread().start(); new FirstThread().start(); } }
运行截图(部分):
可以看出,线程的执行是无序的,线程的执行是根据CPU分配情况来决定的。
2、实现Runnable接口
package cn.thread; public class SecondThread implements Runnable { private int money=500;//卡上还剩500元 @Override public void run() { while(true) { String name = Thread.currentThread().getName(); if (name.equals("Son")) { if (money <= 0) { System.out.println("爸,我没钱了!"); return;//Son线程的run方法结束 } money = money - 300;//儿子一次取300元 System.out.println(name + "卡上还剩:" + money + "元"); } if (name.equals("Father")) { if (money >= 500) { System.out.println("卡上钱还够用!"); return;//Father线程的run方法结束 } money = money + 1000;//父亲一次存1000 System.out.println(name + "卡上还剩:" + money + "元"); } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String args[]) { SecondThread st = new SecondThread(); new Thread(st, "Son").start(); new Thread(st, "Father").start(); } }
运行截图:
继承Thread类和实现Runnable接口该用哪一个?
结论网上一大堆,都可以搜到:继承Thread类不适合资源共享,实现Runnable接口更具有灵活性。
我这里就来解释一下这个结论,深入理解一下它实现的原理
分析一下这两种方法创建线程的原理,看Thread类的源码,如图:
Thread类实现了Runnable接口并重写了run()方法,如果target(目标)为空,则不执行代码。
所以我们这里继承Thread类重写run()方法的原因就在此。代码如下:
public class FirstThread extends Thread { public void run() { //该线程要执行的操作 } public static void main(String args[]) { new FirstThread().start(); } }
继续看,当target不为空时,运行target的run()方法,而变量target看源码
是一个Runnable对象,也就是说只要target这个对象不为空,则调用Runnable对象的run()方法,再把Runnable对象传递给Thread类。代码如下:
public class SecondThread implements Runnable { @Override public void run() { } public static void main(String args[]) { SecondThread st = new SecondThread(); new Thread(st, "线程1").start(); new Thread(st, "线程2").start(); } }
注意看这两行代码:
new Thread(st, "线程1").start(); new Thread(st, "线程2").start();
传给Thread的是同一个Runnable对象,这也就是为什么说实现Runnable接口,可以资源共享,因为操作的都是同一个对象啊。而继承Thread每次都要new一个新的对象,对象自然就不同了。并且,实现Runnable接口这种方式更体现了面向对象这种思维,new一个线程,线程里面传一个对象,这个对象封装了一系列操作。
JAVA线程同步的与通信
主要是以实际例子,应用来说明。
这里先解释一下互斥和同步(之后的缓存池也要用到)
互斥:是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。
同步:上一段代码没的完成,下一段必须等到上一段代码完成后才可以执行(必须严格按照规定的 某种先后次序来运行)。如买票排队
(同步是一种更为复杂的互斥,而互斥是一种特殊的同步。)
异步:上一段代码没的完成,下一段不必等到上一段代码完成就可以执行。如手机发送短信。
1、synchronized同步
线程安全问题这里举个例子用火车购票来解释,库存100张票,三个窗口同时售票。
方法一:直接给方法加上synchronized关键字修饰
package cn.thread; public class SynchronizedSaleTickets implements Runnable { private static int tickets = 100; @Override public void run() { while (tickets>0) {//余票充足 try { Thread.sleep(100);//延迟 } catch (InterruptedException e) { e.printStackTrace(); } sale(); } if(tickets==0){ System.out.println("票已售完!"); } } synchronized static void sale() { tickets--; if(tickets>0) { System.out.println(Thread.currentThread().getName() + "当前余票:" + tickets); } } public static void main(String[] args){ SynchronizedSaleTickets sst=new SynchronizedSaleTickets(); new Thread(sst).start(); new Thread(sst).start(); new Thread(sst).start(); new Thread(sst).start(); } }
运行结果(部分):
方法二:定义一个共同的“锁”
代码和上面大致相同,修改了一点:
package cn.thread; public class SynchronizedSaleTickets implements Runnable { private static int tickets = 100; /* 锁住一个“对象”(门栓),这个对象可以是一个引用对象,也可以是此对象内部的某个数据 这里定义了一个对象 */ private static Object lock=new Object(); @Override public void run() { while (tickets>0) {//余票充足 try { Thread.sleep(100);//延迟 } catch (InterruptedException e) { e.printStackTrace(); } sale(); } if(tickets==0){ System.out.println("票已售完!"); } } static void sale() { synchronized (lock) {//“锁住”要执行的操作 tickets--; if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "当前余票:" + tickets); } } } public static void main(String[] args){ SynchronizedSaleTickets sst=new SynchronizedSaleTickets(); new Thread(sst).start(); new Thread(sst).start(); new Thread(sst).start(); new Thread(sst).start(); } }
注意:锁是上在要操作的资源内部方法中,而不是上在线程代码中!
2、wait、notify通信
这里举一个例子,也是一道java线程面试题
子线程循环10次,接着主线程循环100次,又接着回到子线程循环10次,再接着回到主线程又循环100次,如此循环50次。
代码如下:
package cn.thread; public class ThreadCommunication { public static void main(String[] args) { final Business business = new Business(); new Thread(new Runnable() { @Override public void run() { for (int i = 1; i <= 50; i++) { business.sub(i); } } }).start(); for (int i = 1; i <= 50; i++) { business.main(i); } } //互斥代码放在资源内部方法中,而不是放在线程代码中! static class Business { private boolean shouldSub = true; //子线程业务逻辑 public synchronized void sub(int i) { //这里用while比用if好,用if表示如果被通知唤醒,则继续执行后续的代码,而用while表示被通知唤醒过后 //再回来检查参数是否该执行,逻辑更严密 while (!shouldSub) {//不该子线程执行 try { this.wait();//等待 } catch (InterruptedException e) { e.printStackTrace(); } } for (int j = 1; j <= 10; j++) { System.out.println("sub thread:" + j + ",loop of " + i); } //子线程执行完通知主线程 shouldSub = false; this.notify(); } //主线程业务逻辑 public synchronized void main(int i) { while (shouldSub) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } for (int j = 1; j <= 100; j++) { System.out.println("main thread:" + j + ",loop of" + i); } //主线程执行完通知子线程 shouldSub = true; this.notify(); } } }
运行结果(部分):
3、线程间共享数据
这个例子和火车售票不一样,四个线程,两个线程对每次J+2,两个线程每次对J-1。
注意:共享的数据封装到了一个单独的类中
package cn.thread; public class MultiThreadShareData { public static void main(String[] args) { final ShareData1 data2 = new ShareData1();//共享数据data2 //Thread-0和Thread-1每次-1 new Thread(new MyRunnable1(data2)).start(); new Thread(new MyRunnable1(data2)).start(); ////Thread-2和Thread-3每次+2 new Thread(new MyRunnable2(data2)).start(); new Thread(new MyRunnable2(data2)).start(); } } class MyRunnable1 implements Runnable {//减法 private ShareData1 data1; public MyRunnable1(ShareData1 data1) { this.data1 = data1; } public void run() { while (true) { data1.decrement(); System.out.println(Thread.currentThread().getName() + "线程每次-1,当前:" + data1.getJ()); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyRunnable2 implements Runnable {//加法 private ShareData1 data1; public MyRunnable2(ShareData1 data1) { this.data1 = data1; } public void run() { while (true) { data1.increment(); System.out.println(Thread.currentThread().getName() + "线程每次+2,当前:" + data1.getJ()); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } } class ShareData1 {//共享数据存放在此类的对象中,还有基本的操作 private int j = 0; public int getJ() { return j; } public void setJ(int j) { this.j = j; } public synchronized void increment() { j = j + 2; } public synchronized void decrement() { j--; } }
下篇继续讲解:
线程池(缓存池)
线程相关类
线程面试经典题目
......
原文出自:https://my.csdn.net/qq_37094660(如需转载请注明出处)