java基础(14)- 线程基础

注 : 参考自https://mp.weixin.qq.com/s/MrrN7BDcKMu8SCKs6zij_g

14.1线程概念和两种实现方法

14.2线程示例和传参问题

14.3线程的同步

14.4死锁

14.1线程概念和两种实现方法

线程概念和两种实现方法

一、线程概念

现代的操作系统都是多用户多进程分时操作系统,所以我们在使用操作系统时,可以一边听歌,一边下载,还可以聊天等等,事实上我们的操作系统同时还运行着很多后台进程,你可以打开window系统的任务管理器就可以看到很多进程在运行。

java基础(14)- 线程基础

在往下学习之前,让我们先弄清楚什么是程序,进程和线程这几个概念。

1、程序:利用编程语言开发的一个工具软件, 静态的,在没有启动运行之前只是磁盘中的一个普通文件。

2、进程:程序启动之后就变成了进程,进程是在内存中运行的,具有一定的生命周期,如果程序运行解析,进程在内存中就会回收,生命周期也就结束了,当然程序是可以再次运行,重写变成进程。

3、线程:进程在运行过程中的执行走向,线索。线程是比进程更小的一个单位,一个进程可以有一个或多个线程的。

java程序启动运行时,就自动产生了一个线程,主函数main就是在这个线程上运行的,当不再产生新的线程时,程序就是单线程。到目前为止我们编写的所有的java程序都是单线程的,接下来我们要学习如何实现多线程的java程序。

二、线程的实现方法

1、单线程。我们新建一个类Stu1实现一个简单的循环。

public class Stu1 {

public static void main(String[] args) {
//主线程
for(int i=0;i<10;i++){
System.out.println("主线程:"+i);
}
System.out.println("主线程:死亡");
}

}

运行上面代码,程序只有一个线程,运行完毕程序也就结束了。

2、通过继承Thread类实现第一个线程。新建一个Thread1类继承Thread类,并重写run方法,run方法就是线程的执行内容。实现代码如下:

/**
* 第一种方式
* 1、继承 Thread
* 2、重写run方法
* 3、在主线程中通过start方法启动线程
* @author Administrator
*
*/

public class Thread1 extends Thread{
//线程体
public void run(){
for(int i=0;i<5;i++){
System.out.println("线程1:"+i);
}
System.out.println("线程1:死亡");
}
}

我们要启动这个线程不是调用run()方法,而是使用start()方法启动线程,线程启动后会自动执行run()方法。

修改上面的Stu1的main方法,启动这个线程。修改如下:

public static void main(String[] args) {
Thread1 thread1=new Thread1();
thread1.start();//通过start启动线程,他会自动执行run方法

//主线程
for(int i=0;i<5;i++){
System.out.println("主线程:"+i);
}
System.out.println("主线程:死亡");
}

现在你可以再次运行这个段代码,查看后台的输出。你可以多运行几次,我的其中一次的输出如下:

主线程:0

线程1:0

主线程:1

线程1:1

主线程:2

线程1:2

主线程:3

线程1:3

主线程:4

线程1:4

主线程:死亡

线程1:死亡

由输出可见,主线程和子线程之间是独立运行的,它们将会轮流的占用CPU,而那个线程会占有CPU是由操作系统决定的。所以我们看到多次运行这个程序时,每一次的输出可能都不一样。

3、通过实现Runnable接口实现线程。为了克服java单继承的限制,java提供了另外一种实现线程类的方式,就是实现Runnable接口,因为接口是可以同时实现多个接口的。同样需要实现run方法。实现代码如下:

/**
* 第二种方式
* 1、实现Ruannable接口
* 2、实现run方法
* 3、在主线程通过Thread启动线程
* @author Administrator
*
*/

public class Thread2 implements Runnable{

@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println("线程2:"+i);
}
System.out.println("线程2:死亡");
}

}

启动这个线程也有点区别,你需要new一个Thread类,并把这个实现类作为参数传入这个Thread类,修改Stu1的main方法如下:

public static void main(String[] args) {

Thread1 thread1=new Thread1();
thread1.start();//通过start启动线程,他会自动执行run方法

//启动另外一个线程,该线程实现了Ruannable接口
Thread t2=new Thread(new Thread2());
t2.start();

//主线程
for(int i=0;i<10;i++){
System.out.println("主线程:"+i);
}
System.out.println("主线程:死亡");

}

再次运行该程序,现在程序共有三个线程,每个线程还是独立的,所以输出的结果感觉也是错乱的。

4、其他的方法。事实上面我们可以通过setPriority设置优先级别,当然设置优先级别也只是一个给操作系统一个建议,最后谁先占用CPU还是按照操作系统自己的算法。

另外我们也可以通过sleep()方法让线程休眠,这样他就不会占用CPU了。参考代码如下:

    public static void main(String[] args) {

Thread1 thread1 = new Thread1();
thread1.setPriority(10);
thread1.start();// 通过start启动线程,他会自动执行run方法

Thread t2 = new Thread(new Thread2());
t2.start();

//主线程休眠
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

// 主线程
for (int i = 0; i < 10; i++) {
System.out.println("主线程:" + i);
}
System.out.println("主线程:死亡");

}

运行上面的代码,我们发现主线程的循环要等待两秒后才输出,而线程1的优先级别是最高的。

三、java线程的状态

每个线程都有一个从初始状态到死亡状态的生命周期,其间根据业务流程经过就绪状态(runnable)、运行状态(runing)、阻塞状态(blacked),这些状态可以根据流行需要相互转换。线程状态转换示意图

java基础(14)- 线程基础

0、初始状态跟其他Java对象一样用new语句创建的线程对象处于初始状态,此时仅被分配了内存。

1、就绪状态(Runnable)

当每个线程对象创建好后,通过调用其start()方法使得该线程就进入就绪状态,就绪状态的线程进入到JVM的可运行池中等待CPU的使用权,每个线程都可以通过设置其优先级以提高取得CPU的使用权的可能性。当然,CPU的使用权跟计算机的硬件分不开的,这体现在单CPU环境下跟多CPU(及单CPU多核)下处理是不一样的,因为同一时刻可以让几个线程占用不同的CPU。

2、运行状态

在JVM中就绪状态的线程取得CPU的使用权后,得以执行run()方法中定义的程序代码。只有处于就绪状态的线程才有机会转到运行状态。

3、阻塞状态(Blocked)

运行中的线程可以根据需要放弃占用的CPU而暂停运行,这是线程就处于阻塞状态,此时Jvm不再尝试给线程分配CPU使用机会。处于阻塞状态有三种情况:

1.位于Jvm线程等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,参见第20章的20.1.1节(同步代码块)。

2.位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,参见第20章的20.2节(线程通信)。

3.其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求,就会进入这个状态。

4、死亡状态(Deaded)

当线程正常执行完run()方法中的代码,或者遇到异常未捕获造成中断,线程就会退出运行状态,转入死亡状态,结束其生命周期。


14.2线程示例和传参问题

线程示例和传参问题

为了使大家对线程有更深的认识,这里举两个简单的例子说明一下:

一、倒计时示例

这个例子比较简单,要求程序每一秒中就输出一个数字,模拟倒计时。要实现每隔一秒才输出,只需要让程序输出一个数字就休眠1秒的时间,实现代码如下:

public class ThreadDaoji extends Thread{

public void run(){
for(int i=10;i>0;i--){
System.out.println(i);
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}

在main函数中启动这个线程,后台将会间隔1秒输出一个,当run方法执行完毕,该线程也就结束了。代码如下:

public class Stu2 {
public static void main(String[] args) {
ThreadDaoji threadDaoji=new ThreadDaoji();
threadDaoji.start();
}
}

二、闹钟示例和传参问题

这个示例要求用户设置时和分两个世界,当到达指定的时间,程序就会发生连续的声响,提示用户时间已经到了。要实现这个功能。

我们首先讲解一下线程的传参的问题,因为线程必须实现run方法,该方法是在父类定义的,所以我们要给线程传参数,就不能通过run()方法传递的,只能定义类的全局变量,我们可以使用构造方法传参或者给全局变量设置set的方法来实现传参。比如下面这个闹钟类,就是用构造方法传参,代码如下:

public class ThreadAlarm extends Thread{

private int hour;
private int miniute;

public ThreadAlarm(int hour,int miniute){
this.hour=hour;
this.miniute=miniute;
}


public void run(){

}

}

run()方法就是实现闹钟的判断功能,程序每隔一秒钟就会获得当前时间,然后与用户传进来的时间进行比较,如果时间到了就发出声音,然后线程才结束。run()方法实现如下:

    public void run(){
while(true){
//当前的时间是否等于用户设定的时间,每一秒都做一次判断
Calendar calendar=Calendar.getInstance();
//当前的时间
int curhour=calendar.get(Calendar.HOUR);
int curminute=calendar.get(Calendar.MINUTE);

if(hour==curhour&&miniute==curminute){
//时间到了。发出5下声响
for(int i=0;i<5;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
java.awt.Toolkit.getDefaultToolkit().beep();
}

break;
}
}
}

完成后,我们就可以在main方法中调用这个线程,要求用户输入时和分两个变量。实现代码如下:

public class Stu3 {

public static void main(String[] args) {
//
Scanner scanner=new Scanner(System.in);
System.out.println("时:");
int hour=scanner.nextInt();
System.out.println("分:");
int minute=scanner.nextInt();

ThreadAlarm alarm=new ThreadAlarm(hour, minute);
alarm.start();
System.out.println("闹钟启动了。");
}

}

你可以运行以上程序进行测试。


14.3线程的同步


一、什么是线程的同步

1、线程同步的意义。

线程的同步是为了保证代码的原子性,保证每个线程在调用对象同步方法独占的方法操作该对象。一段代码就好像一座独木桥,任何一个时刻,只能有一个人在桥上行走,程序中不能有多个线程同时在这两句代码之间执行,这就是线程同步。比如:银行的自动柜员机,一次只能一个人取钱,其他人必须排队。公司的打印机虽然是多台电脑共用的,但是每次也只能一台电脑在打印。

同步是以牺牲程序的性能为代价的。所以,如果确定程序没有安全性的问题,就没有必要使用同步。

2、同步的实现,使用synchronize关键字

我们将需要具有原子性的代码放入synchronize语句内,形成同步代码块。就可以保证线程安全了。每个对象都有一个标志位(锁旗标),该标志位有两个状态0、1,其开始状态为1,当执行synchronized(Object)语句后,Object对象的标志位变为0状态,直到执行完整个synchronized语句中的代码块后才又回到1状态。

一个线程执行到synchronized(Object)的时候,先检查Object对象的标志位,0表示有线程在执行,这个线程将暂时阻塞,让出CPU资源,直到另外线程执行完有关代码,将Object对象状态恢复到1状态,这个阻塞才被取消,然后线程开始执行,同时将Object对象的标志位变为0,防止其他线程再进入有关的同步代码块中。

当线程执行到synchronized的时候,如果得不到锁标记,这个线程会被加入到一个与该对象的锁标记相关连的等待线程池当中,等待锁标记。当线程执行完同步代码,就会释放锁标记。

synchronized有两种用法

(1)定义同步方法: synchronized 方法名称(同步){ 方法体 } ,使用这个方法对象都会同步。

(2)同步块,灵活锁住对象同步方法就等于锁住this:synchronized(对象){ 同步块 }

二、示例代码

下面我们模拟公司有一台打印机,有多个学生同时操作电脑,点击打印自己的成绩,如果不使用同步,打印机打印出来的结果将会错乱,然后我们改成使用同步后,再查看结果,打印机会打印一个学生后,再打印另外一个学生,有序的把所有的学生成绩打印出来。

1、先定义打印机类,暂时不实现同步,代码如下:

/**
* 打印机
*/

public class Print {

public void p(String name,int en,int math){
System.out.print("姓名:"+name+":");
System.out.println("数学成绩:"+math+",英语成绩:"+en);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

2、实现学生打印操作类,每个操作类就是一个线程,该线程调用同一个打印对象实现打印。代码如下:

public class StudentThread extends Thread {

private Print print;
private String name;
private int en;
private int math;

public StudentThread(Print print,String name,int en,int math){
this.print=print;
this.name=name;
this.en=en;
this.math=math;
}

@Override
public void run() {
print.p(name, en, math);
}
}

3、在main方法中定义一个打印机对象,和三个学生操作类线程,三个线程同时启动,代码如下:

public class Run {
public static void main(String[] args) {
Print print=new Print();

StudentThread stu1=new StudentThread(print, "诸葛亮", 100, 100);
StudentThread stu2=new StudentThread(print, "张飞", 10, 40);
StudentThread stu3=new StudentThread(print, "刘备", 80, 90);

stu1.start();
stu2.start();
stu3.start();

}

}

运行上面的代码,控制台打印如下:

java基础(14)- 线程基础

可以看到所有人的成绩都错乱了,根据不知道那个成绩是那一个学生的。要防止这种情况非常简单,只需要在打印类中的打印方法前面加上synchronized关键字实现同步即可,修改打印机类如下:

public class Print {

public synchronized void p(String name,int en,int math){
System.out.print("姓名:"+name+":");
System.out.println("数学成绩:"+math+",英语成绩:"+en);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

再次运行代码,结果如下:

java基础(14)- 线程基础


14.4死锁

一、死锁

同步有可能会导致另一个问题,就是死锁。死锁是指两个线程,都相互等待对方释放,死锁有时是不可测知或避开的,我们应该应采取措施避免死锁的出现。

二、死锁的例子

当然,这是一个反面例子。

1、定义一个资源类,为了标识每个资源,该类只有一个属性,就是资源名称。代码如下:

public class Resource {

private String Name;

public String getName() {
return Name;
}

public void setName(String name) {
Name = name;
}
}

2、定义一个资源使用类,该类将会使用到两个资源对象,都是先锁住第一个时,然后在企图锁定另外一个。代码如下:

public class ResourceThread extends Thread{

private String threadName;//线程名称

private Resource rs1;
private Resource rs2;

public ResourceThread(String threadName,Resource rs1,Resource rs2){

this.threadName=threadName;
this.rs1=rs1;
this.rs2=rs2;

}

public void run(){

//
System.out.println(threadName+"企图占有"+rs1.getName());
synchronized (rs1) {
System.out.println(threadName+"已经锁定了"+rs1.getName());

try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(threadName+"企图占有"+rs2.getName());
synchronized (rs2) {
System.out.println(threadName+"已经锁定了"+rs2.getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
System.out.println(threadName+"释放"+rs2.getName());

}
System.out.println(threadName+"释放"+rs1.getName());

}

}

3、模拟,现在只有三个资源对象,三个线程对象,

线程一锁住资源一,其他占用资源二;

线程二锁住资源二,其他占用资源三;

线程三锁住资源三,其他占用资源一;

示意图如下:

java基础(14)- 线程基础

代码实现如下:

public class Run {

public static void main(String[] args) {

Resource resource1=new Resource();
resource1.setName("资源一");

Resource resource2=new Resource();
resource2.setName("资源二");

Resource resource3=new Resource();
resource3.setName("资源三");

ResourceThread thread1=new ResourceThread("线程一", resource1, resource2);
ResourceThread thread2=new ResourceThread("线程二", resource2, resource3);
ResourceThread thread3=new ResourceThread("线程三", resource3, resource1);

thread1.start();
thread2.start();
thread3.start();

}

}

结果是哪一个线程都不能正常结束,程序也就一直耗着。你也只能终止这个程序了。