说在前面,我自己是不喜欢看特别长篇的博客。我能理解看长篇博客的那种难受,因此,想直接入正题看编码的不妨跳过前两块内容,直接进入“线程的创建”。
线程概述
什么是线程?
之前看到过某大神这样一句话:比较糟糕的技术文档的特征:用专业名词来解释专业名词
从这句话中,我受益匪浅,因此以后的文章尽量都避免这样。相信大家也不喜欢看一些概念性的东西吧。
介绍线程之前,首先得解释一下进程,因为要用进程来解释线程。
进程:是程序的一次动态执行过程。比如打开谷歌浏览器,从它打开的时刻开始就启动了一个进程。
多进程:多进程就像打开了多个程序,比如边玩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.youkuaiyun.com/qq_37094660(如需转载请注明出处)