Java:多线程

Java基础

浏览数:225

2019-6-30


本文内容:

  • 什么是线程
  • 线程的生命周期
  • Thread实现的多线程
  • Runable实现的多线程
  • 线程常用函数
  • 线程的控制
  • 线程同步
  • 线程通信

首发日期:2018-05-13

Thread实现的多线程:

实现方法:

  1. 定义一个类继承Thread。
  2. 覆盖run方法,将自定义代码写到run方法中
  3. 创建子类对象就是创建线程对象
  4. 子类调用Thread类中的start方法就可以执行线程,并会调用run方法。
class MyThread extends Thread{
    public void run() {
        for (int i=0;i<10;i++) {
            System.out.println("子线程拿到执行权");
        }
    }
}

public class Demo {

    public static void main(String[] args) {
        MyThread t=new MyThread();
        t.start();
        for (int i=0;i<10;i++)
            System.out.println("主线程运行");
    }

}

上述代码结果【该结果有随机性,如果想要有明显的抢夺运行权,可以增大i】:

主线程运行
子线程拿到执行权
子线程拿到执行权
子线程拿到执行权
子线程拿到执行权
子线程拿到执行权
主线程运行
主线程运行
主线程运行
主线程运行

补充:

  • 执行run与start的区别:执行run仅仅相当于调用函数,并没有创建线程。而start是开启线程,并让开启的线程去执行run方法中的线程任务

Runable实现的多线程:

虽然已经有了继承Thread实现的多线程,但是由于在java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类 ,因为这样,所以才有了Runable实现的多线程,这样的多线程是将实现接口Runable的类的对象传入Thread()中来创建线程对象。

  • Runable是一个接口,Thread类实现了这个接口

实现方法:

  1. 定义一个类实现Runnale接口,重写run方法。 【run的权限是public的】
  2. 将这个类的一个对象传入Thread()中,使用Thread类直接创建线程对象。
  3. 线程对象调用start()。
class Car implements Runnable{
    public void run() {
        for (int i=0;i<5;i++) {
            System.out.println("子线程拿到执行权");
        }
    }
}

public class Demo {

    public static void main(String[] args) {
        Car c=new Car();
        Thread t=new Thread(c);
        t.start();
        for (int i=0;i<5;i++)
            System.out.println("主线程运行");
    }

}

上述代码结果【该结果有随机性,如果想要有明显的抢夺运行权,可以增大i】:

主线程运行
子线程拿到执行权
子线程拿到执行权
子线程拿到执行权
子线程拿到执行权
子线程拿到执行权
主线程运行
主线程运行
主线程运行
主线程运行

两种方式的区别:

  • Runnable的多个线程实例可以使用同一个实例变量,而继承Thread实现的线程无法共享同一个实例变量
  • Runnable是实现,java允许多实现,不允许多继承,所以使用实现Runnable的同时可以继承其他类。

补充:

  • 还有实现callable接口的方式可以创建新线程。
  • 上面的两种实现方式都不可以获取返回值,获取返回值应该使用callable的方式来创建新线程

线程常用函数:

  1. getId():返回该线程的标识符
  2. getName():返回线程的名字 【在实现Runnable的类中,没有线程对象,所以需要变成Thread.currentThread().getName();】
  3. setName():给线程设置名字【在new Thread()时可以传入一个参数作为线程的名字】
  4. currentThread():返回当前执行的线程对象
  5. getState():返回线程的状态
  6. isAlive():判断线程是否处于活动状态,返回布尔值
  7. join(int seconds):等待线程结束,有seconds代表最多等待秒数 【比如某个线程调用join(),主线程会等待这个线程执行完毕才会返回主线程执行】
  8. sleep(int seconds):让线程暂停指定秒数
  9. wait():让线程暂停
  10. interrupt():中断线程。【对于非堵塞线程,那么就会将线程中断;对于可取消的阻塞状态中的线程(Thread.sleep(), Object.wait(), Thread.join(), ),那么可以“吵醒”线程】
  11. isDaemon():判断线程是否是守护线程
  12. setDaemon():设置成守护线程
  13. setPriority():更改线程的优先级
  14. yield():暂停该线程,让出CPU执行权,重新到队列中竞争CPU权限

线程的控制:

线程等待:

  • join():join函数实现的效果是,如果某个线程调用join函数,那么会等到这个线程完全执行完毕才会轮到其他线程继续执行。
    • 也可以提供参数,join(n)代表等待线程执行n毫秒

守护线程

默认情况下,主线程会等待其他线程结束才会结束程序运行,设置守护线程的效果是:如果一个线程设置成了守护线程,那么主线程不会等待该线程结束就结束程序运行(如果非主线程就只有这一个的话)

  • 线程.setDaemon(true);
  • setDaemon必须要在线程start之前调用。

线程睡眠:

  • sleep(毫秒数):线程调用sleep函数可以使线程暂停一段时间,让线程进入堵塞状态。

改变线程优先级:

  • setPriority(int newPriority);优先级高的会更容易获得CPU执行权限
  • 右边是一些常量,默认优先级是5

线程让步:

yield():让出自己的CPU权限,重新进入队列中竞争CPU权限【这时候优先级高的占便宜】

线程同步:

为什么需要线程同步:

  • 当有多个线程操作同一变量时,如果不能统一的执行完整操作,那么可能会发生线程A未执行完成,线程B过来操作变量,导致后面线程A操作这个变量时已经不是它之前取到的变量值。
  • 线程同步的实质:到了需要线程安全的代码,由并行改成串行,只允许一个进程执行。

同步的方式:

  • 同步代码块:synchronized(同步锁对象){同步代码}  【对于通过实现Runnable得出的多线程一般同步锁对象是this或者类名.class【或者是一些共享的对象】,理论上也同步锁对象可以任意的对象,但应该避免使用不必要的资源来作为同步锁】 【也因为同步代码块的同步锁对象可以比较随便,所以开放性比较强,使得一个类中不同的同步代码块可以使用不同的锁】
  • 同步函数:synchronized 返回值类型 方法名(参数列表){同步代码}
    • 当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
    • 非静态函数的同步锁默认是this,静态同步函数的同步锁所任是字节码文件对象”类名.class”

同步锁:

  • jdk5提供了新的同步锁对象来显示使用同步锁,对于同步代码块来说,在多个使用时需要使用多个锁,锁的意义并不是很明显,而同步锁类创建的对象就带有同步锁的意义。
  • 常见的同步锁类(这几个锁类都实现Lock接口)有:ReentrantLock(可重入锁,递归锁),ReentrantReadWriteLock.ReadLock(读取锁),ReentrantReadWriteLock.WriteLock(写锁) 【一般都使用ReentrantLock】
  • 它的操作是在需要锁住的代码之前lock对象.lock(),在操作结束后 lock对象.unlock(),

死锁:

当使用上锁后,可能会发生死锁。A拿了锁1,A想要拿锁2;B拿了锁2,B想要拿锁1;于是就发生了死锁。

解决方法:只能避免。避免相互调用彼此的锁(或者说某种独占资源)

线程通信:

线程通信最经典的例子是生产者-消费者例子:生产者生产完后提醒一下消费者来消费,消费者消费完后提醒生产者生产。【一个盘子时,生产者生产完就得叫消费者;多个盘子时,生产者判断是否没有空盘子再自己进行等待,消费者判断没有东西消费就等待】

传统方式:

  • 对于还没轮到的,对象调用wait方法,等待别人唤醒自己 【wait方法调用者不是线程对象,是这个监视器(或者说是锁),如果你的锁是this时不需要前缀,不然需要调用锁对象来调用。】
  • 当想唤醒另外一方时,调用notify方法来唤醒。【notify是唤醒任意一个在等待锁的进程,notifyAll是唤醒所有在等待锁的进程】
class RestRoom{
    int count=0;
    boolean panzi=true;
    
    public synchronized void  produce() {
        try {
            if(!panzi) {
                this.wait();
            }
            count++;
            System.out.println("我生产了一个面包"+count);
            panzi=false;
            this.notify();
            
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public synchronized void consume() {
        try {
            if(panzi) {//有空盘子
                this.wait();
            }
            System.out.println("我消费了一个面包"+count);
            panzi=true;
            this.notify();
            
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

class Producer  implements Runnable{
    RestRoom r;
    Producer(RestRoom r){
        this.r=r;
    }
    public void run() {
        for(int i=0;i<100;i++) {
            r.produce();//生产100次
        }
    }
}

class Consumer  implements Runnable{
    RestRoom r;
    Consumer(RestRoom r){
        this.r=r;
    }
    public void run() {
        for(int i=0;i<100;i++) {
            r.consume();//生产100次
        }
    }
}


public class TongbuDemo {

    public static void main(String[] args) {
        RestRoom r=new RestRoom();
        Producer pro=new Producer(r);
        Consumer con=new Consumer(r);
        Thread t1=new Thread(pro);
        Thread t2=new Thread(con);
        t1.start();
        t2.start();
    }
}

使用注意:

  • 如果只有一个生产者,一个消费者,在等待唤醒的只有可能是“对方”;但如果存在多个生产者或多个消费者时,那么可能会唤醒“本方”,这时可以使用notifyAll(),唤醒所有的本方,然后本方再通过判断条件来wait【一种常用的先同再异的思想】【同时因为有可能已经有线程之前就进入了判断环节,所以需要使用while才能把它留在判断环节中】。
package runable_线程;


class RestRoom{
    int count=0;
    boolean panzi=true;
    
    public synchronized void  produce() {
        try {
            while(!panzi) {
                this.wait();
            }
            count++;
            System.out.println("我生产了一个面包"+count);
            panzi=false;
            this.notifyAll();
            
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public synchronized void consume() {
        try {
            while(panzi) {//有空盘子
                this.wait();
            }
            System.out.println("我消费了一个面包"+count);
            panzi=true;
            this.notifyAll();
            
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

class Producer  implements Runnable{
    RestRoom r;
    Producer(RestRoom r){
        this.r=r;
    }
    public void run() {
        for(int i=0;i<100;i++) {
            r.produce();//生产100次
        }
    }
}

class Consumer  implements Runnable{
    RestRoom r;
    Consumer(RestRoom r){
        this.r=r;
    }
    public void run() {
        for(int i=0;i<100;i++) {
            r.consume();//生产100次
        }
    }
}


public class TongbuDemo {

    public static void main(String[] args) {
        RestRoom r=new RestRoom();
        Producer pro=new Producer(r);
        Producer pro2=new Producer(r);
        Consumer con=new Consumer(r);
        Consumer con2=new Consumer(r);
        Thread t1=new Thread(pro);
        Thread t2=new Thread(pro2);
        Thread t3=new Thread(con);
        Thread t4=new Thread(con2);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

新增了lock接口的一系列实现类后,可以使用Condition来进行线程通信:

  • 可以使用 同步锁对象.newCondition() 来获取Condition对象
  • Condition中等待的方式变成了condition.await();提醒的方式变成了condition.sign()和condition.signAll();
  • 新的Condition对象允许一个锁能有多个Condition对象,所以我们可以使用不同的Condition对象来代表不同的身份。

在单一的生产者和消费者时,代码与上面相同大略。

当多个多个生产者或多个消费者时,那么就需要定义不同的Condition对象了,不同的condition对象,唤醒的线程也不一样,比如可以定义一个condition对象专门代表消费者,那么使用这个对象.signal()时就会唤醒消费者。

package runable_线程;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class RestRoom{
    int count=0;
    boolean panzi=true;
    ReentrantLock lock=new ReentrantLock();
    Condition condition_pro = lock.newCondition();
    Condition condition_con = lock.newCondition();
    public  void  produce() {
        lock.lock();
        try {
            if(!panzi) {
                condition_pro.await();
            }
            count++;
            System.out.println("我生产了一个面包"+count);
            panzi=false;
//            this.notifyAll();
            condition_con.signal();//唤醒消费者
            
        }catch(InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    
    public void consume() {
        lock.lock();
        try {
            if(panzi) {//有空盘子
                condition_con.await();
            }
            System.out.println("我消费了一个面包"+count);
            panzi=true;
//            this.notifyAll();
            condition_pro.signal();//唤醒生产者
            
        }catch(InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    
}

class Producer  implements Runnable{
    RestRoom r;
    Producer(RestRoom r){
        this.r=r;
    }
    public void run() {
        for(int i=0;i<100;i++) {
            r.produce();//生产100次
        }
    }
}

class Consumer  implements Runnable{
    RestRoom r;
    Consumer(RestRoom r){
        this.r=r;
    }
    public void run() {
        for(int i=0;i<100;i++) {
            r.consume();//生产100次
        }
    }
}


public class TongbuDemo {

    public static void main(String[] args) {
        RestRoom r=new RestRoom();
        Producer pro=new Producer(r);
        Producer pro2=new Producer(r);
        Consumer con=new Consumer(r);
        Consumer con2=new Consumer(r);
        Thread t1=new Thread(pro);
        Thread t2=new Thread(pro2);
        Thread t3=new Thread(con);
        Thread t4=new Thread(con2);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

补充:

  • 同步中,线程方法的wait和sleep的区别:wait()会释放自己已经拿到的同步锁,而sleep不会释放自己拿到的同步锁。

 

作者:人道浮沉