Java与多线程

2018-12-20
学习笔记

Java与多线程


  • 并发:世间万物都可以同时完成很多工作,比如人体可以同时完成呼吸,血液循环,思考问题,用户使用计算机可以听歌,打印文件,玩游戏,这些活动完全可以同时进行,这种思想在Java中被成为并发,而将并发完成的每一件事情成为线程。
  • 多线程:并非所有的程序语言都支持线程,比如JavaScript,但在Java中提供了并发机制,程序员可以在程序中执行多个线程,每一个线程完成一个功能,并与其它线程并发执行,这种机制被称为多线程。

操作系统与多线程


多线程在不同的系统中的表现是不同的。

  • Java的多线程在Windwos下:Windwos操作系统是多任务操作系统,它以进程为单位。一个进程是一个包含有自身地址的程序,每个独立执行的程序都称为进程,也就是正在执行的程序。系统可以分配给每个进程一段有限的使用CPU的时间(也可以称为CPU时间片),CPU在这段时间中执行某个进程,然后下一个时间片又跳至另一个进程中去执行。由于CPU转换较快,所以使得每个进程好像是同时执行一样。

Java实现多线程


实现Java多线程有两种方法,分别为继承java.lang.Thread类、和实现java.lang.Runnable接口。

继承java.lang.Thread类

  1. 继承

    public class ThreadTest extends Thread{

    }
  2. Thread类构造方法

    public Thread(); //创建一个新的线程对象
    public Thread(String threadName); //创建一个名称为threadName的线程对象
  3. 实现run()方法
    完成线程真正功能的代码放在类的run()方法中。当一个类继承Thread类之后,就可以在该类中覆盖run()方法,将实现该线程的代码放入run()方法中。

    @override 
    public void run()
    {

    }
  4. 启用线程,调用run()方法

    调用Thread类中的start()方法执行线程,也就是run()方法。

    public static void main(String []args)
    {
    new ThreadTest().start();
    }

    当start()方法调用一个已经启动的线程,系统将抛出IllegalThreadStateException异常

  5. 实例

    public class ThreadTest extends Thread{     //继承Thread类
    private int count = 10;
    public void run(){
    while(true){
    System.out.print(count+" ");
    if(--count == 0){
    return;
    }
    }
    }
    public static void main(String []args){
    new ThreadTest().start(); //调用run()方法
    }
    }

    运行结果为

    /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/....
    10 9 8 7 6 5 4 3 2 1
    Process finished with exit code 0

实现java.lang.Runnable接口

如果程序员需要继承其他类(非Thread类),而且还要使当前类实现多线程,那么可以通过Runnable接口来实现。实际上,Thread类实现了Runnable接口,其中的run()方法正是对Runnable中的run()方法的具体实现。

实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象与Thread对象相关联。Thread类有一下两个构造方法:

public Thread(Runnable target);
public Thread(Runnable target,String name);

这两个构造方法的参数都存在Runnable实例,使用以上构造方法就可以将Runnable实例与Thread实例相关联。

使用Runnable接口启动新的线程的步骤如下:

  1. 建立Runnable对象。

  2. 使用参数为Runnable对象的构造方法创建Thread实例。

  3. 调用start()方法启动线程。

实例:

Thread t = new Thread(new java.lang.Runnable() {
private int count = 10;
@Override
public void run() {
while(true){
System.out.println("Runnable线程输出"+count);
if(--count == 0){
return;
}
}

}
});
t.start();

线程的生命周期


  • 出生:在线程实例start()方法被调用之前都是出生期
  • 就绪:当调用start()后,线程处于就绪状态(又称为可执行状态)
  • 运行:当线程得到系统资源后就进入运行状态

一旦线程进入可执行状态,它会在就绪与运行状态下切换,同时也有可能进入等待,休眠,阻塞或死亡状态。

  • 等待:当处于运行状态下的线程调用Thread类中的wait()方法时,该线程便进入等待状态
  • 休眠:当线程调用Thread类中的sleep()方法时,则会进入休眠状态
  • 阻塞:如果一个线程在运行状态下发出输入/输出请求,该线程将会进入阻塞状态,在其等待输入输出结束时线程进入就绪状态。
  • 死亡:当线程的run()方法执行完毕,线程进入死亡状态

线程的休眠


线程的休眠依靠调用sleep()方法实现,sleep()方法需要一个参数用于指定线程休眠的毫秒数。

public class ThreadTest extends Thread{
public void run(){

//sleep()方法在run()方法中调用
try{
Thread.sleep(毫秒数);
}catch(Exception e){
e.printStackTrace();
}

}
}

其中,try和catch是语法中规定必须写的,sleep()中接受的是一个毫秒数。

线程的加入


引言:假如一个读者正在看电视,突然有人上门收水费,读者必须付完水费后才可以继续看电视。

就像引言说的,当一个线程A需要插入线程B,并要求线程B先执行完毕,然后再执行线程A,此时可以使用Thread类中的join()方法来完成。

public class Test
{
public static void main (String []args)
{
Thread b = new Thread (new Runnable(){
private int count =30;
public void run()
{
while(true){
System.out.println("B线程运行"+count);
if(--count == 0){
return;
}
}
}
});
Thread a = new Thread(new Runnable(){
private int count = 10;
public void run()
{

while(true){
System.out.println("A线程运行"+count);
if(--count == 0){
return;
}
try{
b.join(); //调用b
}catch(Exception e)
{
e.printStackTrace();
}
}
}
});
a.start();
b.start();
}
}

运行结果为

B线程运行30
..
B线程运行13
A线程运行10
B线程运行12
....
B线程运行1
A线程运行9
A线程运行8
A线程运行7
A线程运行6
A线程运行5
A线程运行4
A线程运行3
A线程运行2
A线程运行1

b启动后被a的run()中的b.join()调用,a线程因此会进入等待状态,等待b线程运行完成才继续运行。

线程的中断


新的JDK已经废除了stop()方法,不建议使用stop()方法来停止一个线程的运行。现在提倡在run()方法中使用无限循环的形式,然后使用一个布尔型标记控制循环的停止。

public class Test{
public static void main (String []args){
Thread a = new Thread(new Runnable(){
//设置一个标记变量控制循环是否继续运行,默认值为flase
private boolean isStop = false;
public void run() {
int count = 0;
while(true)
{
if(count == 100)isStop=true;
System.out.println("A线程正在工作"+count++);
if(isStop)break;
}
}
});
a.start();
}
}

运行结果如下

A线程正在工作1
...
A线程正在工作100

线程的礼让


Thread类中提供了一种礼让方式,使用yield()方法表示,它只是给当前正处于运行状态的线程一个提醒,告知它可以将资源礼让给其他线程,但这仅仅只是一种暗示,没有任何一种机制保证当前线程会将资源礼让。

yield()方法使具有同样优先级的线程有进入可执行状态的机会,当当前线程放弃执行权时会再度回到就绪状态。对于支持多任务的操作系统来说,不需要调用yield()方法,因为操作系统会为线程自动分配cpu时间片来执行

线程的优先级


Thread 类中包含的成员变量代表了线程的某些优先级,如Thread.MIN_PRIORITY(常数1)、Thread.MAX_PRIORITY(常数10)、Thread.NORM_PRIORITY(常数5)、Thread.MIN_PRIORITY~Thread.MAX_PRIORITY 之间。

在默认情况下其优先级都是Thread.NORM_PRIORITY,每个新产生的线程都继承了父线程的优先级。

线程同步


两个人同时说话,两个人同时过同一个独木桥,就会发生抢占现象,在多线程中需要防止这些资源访问的冲突,Java提供了线程同步的机制来防止资源访问的冲突。

实际开发中,使用多线程程序的情况很多,如银行排号系统、火车站售票系统等。这种多线程的程序通常会发生问题,以火车票售票系统为例,在代码中判断当前票数是否大于0、但当两个线程同时访问这段代码时(加入这时只剩下一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断当前是否有票的操作,并得出票数大于0的结论,于是它也执行售出操作,这样就会产生负数。所以编写多线程程序的时,应该考虑到线程安全问题。线程安全问题本质上来源于两个线程同时存取单一对象的数据

import static java.lang.Thread.sleep;
public class Test implements Runnable{
int num = 10; //设置当前总票数
public void run(){
while(true){
if(num > 0){
try {
sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("tickets"+num--);
}
}
}
public static void main (String []args){
Test t = new Test(); //实例化类对象
Thread tA = new Thread(t);
Thread tB = new Thread(t);
Thread tC = new Thread(t);
Thread tD = new Thread(t);
tA.start(); //分别启动线程
tB.start();
tC.start();
tD.start();
}
}

运行结果为

tickets10
tickets9
tickets10
tickets8
tickets7
tickets5
tickets6
tickets4
tickets3
tickets3
tickets2
tickets1
tickets0
tickets-1
tickets-2

以上实例中最后打印的票数为负值,这样就出了问题,这是由于同时创建了四个线程,这四个线程执行run()方法,在num变量为1时,线程1、2、3、4都对num变量有存储功能,当线程1执行run()方时,还没有来的级做递减操作,就指定它调用sleep()方法进入休眠状态,此时线程2、3、4都进入了run()方法,发现num变量依然大于0,但此时线程1休眠时间已到,将num变量值递减,同时线程2、3、4也都对num变量进行递减操作。

线程同步机制


基本上所有解决多线程冲突问题的方法都是采用给定时间只允许一个线程访问共享资源,这时就需要给共享资源上一道锁。这就好比一个人上洗手间,他进入洗手间后会将门锁上,出来时再将锁打开,然后其他人才可以进入。

同步块

在java中提供了同步机制,可以有效防止资源冲突。同步机制使用synchronized关键字,

package test;

import static java.lang.Thread.sleep;
public class Test implements Runnable{
int num = 10;
public void run(){
while(true){
synchronized(""){
if(num > 0){
try{
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
System.out.println("tickets"+ --num);
}
}
}
}
public static void main(String []args){
Test t = new Test();
Thread tA = new Thread(t);
Thread tB = new Thread(t);
Thread tC = new Thread(t);
Thread tD = new Thread(t);
tA.start();
tB.start();
tC.start();
tD.start();

}
}

运行结果

/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java...
tickets9
tickets8
tickets7
tickets6
tickets5
tickets4
tickets3
tickets2
tickets1
tickets0

通常将共享资源的操作放置在synchronized定义的区域内,这样当其他线程也获取到了这个锁时,必须等待锁被释放时才能进入该区域。

synchronized(Object){

}

同步块是如何保证单一线程运行的?Object为任意一个对象,每个对象都存在一个标志位,并具有两个值,分别为0和1.一个线程运行到同步块时首先检查该对象的标记位,如果为0状态,表明此同步块中存在其他线程在运行。该线程则进入就绪状态直到同步块中的线程执行完代码并且标记为被设置为1后,该线程才能执行同步块中的代码。

同步方法

同步方法就是在方法前面修饰synchronized 关键字的方法,其语法如下:

synchronized void f(){}

当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为synchronized,否则就会出错。


public class Test extends Thread{
public synchronized void doit(){ //定义同步方法
if(num > 0){
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("tickets"+ --num);
}
}
public void run(){
while(true){
doit(); //在run()方法中调用该同步方法
}
}
public static void main(String []args){
Test a = new Test();
a.start();
}
}