Java基础(六)

多线程

JVM允许创建多个线程,它通过java.lang.Thread类来体现

创建方式

多线程的创建方式有四种

  • 继承于Thread类
  1. 创建一个继承于Thread类的子类

  2. 重写Thread类的run()方法

  3. 创建Thread类的子类的对象

  4. 调用该对象的start()方法

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();//启动该线程并调用run方法
        //下面代码仍然在main线程执行
        System.out.println("hello");
    }
}
复制代码

已经start的线程不可以再次start,主线程通过Thread.currentThread获取

  • 写class去实现Runabble接口
  1. 创建一个实现了Runabble接口的类

  2. 实现Runabble中的抽象方法Run()

  3. 创建实现类的对象

  4. 将此对象传到Thread类的构造器中,创建Thread类的对象

  5. 通过Thread类的对象调用start()方法

class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println("111");
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread my=new MyThread();
        Thread thread1=new Thread(my);
        Thread thread2=new Thread(my);
        thread1.start();
        thread2.start();
    }
}
复制代码

两种方式的对比:

开发当中优先选择实现Runabble接口的方式。原因:Runabble没有单继承的局限性,并更适用于多线程有共享数据的情况

新增的两种创建方式:

  • 实现Callable接口
//1. 创建一个实现了callable接口的实现类
class MyThread implements Callable {
    //2. 实现Call方法,将此线程需要执行的操作声明在Call方法中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
            sum += i;
        }
        return sum;
    }
}

public class Test {
    public static void main(String[] args) {
        //3. 创建Callable接口实现类的对象
        MyThread mt = new MyThread();
        //4. 将Callable接口实现类的对象作为参数传递到FutureTask构造器中,并创建FutureTask对象
        FutureTask ft = new FutureTask(mt);
        //5. 将FutureTask对象作为参数传递到Thread构造器中,并调用start()
        new Thread(ft).start();
        try {
            //FutureTask的get()得到的是构造器中形参的Callable对象的重写的Call()方法的返回值
            Object sum = ft.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
复制代码
  • 使用线程池

提前创造好多个线程放到线程池中,使用时直接获取,使用完成后放回池子中,避免频繁的创建销毁,可以重复利用。

class MyThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        //1. 提供指定数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //2. 提供一个实现Runnable接口或Callable接口的类的对象
        MyThread myThread = new MyThread();
        service.execute(myThread);//适用于Runnable
        service.submit(myThread);//适用于Callable

        service.shutdown();//关闭线程池
    }
}
复制代码
Thread类的常见方法使用

image.png

image.png

线程优先级

对于同优先级的线程,采用队列方式分配资源,对于高优先级则使用优先调度的抢占式策略

可以通过getPriority()来查看线程优先级或者Thread.currentThread.setPriority()设置优先级,高优先级的线程有高的概率抢占低优先级的线程。默认的优先级为5

线程的生命周期

线程的生命周期包含下面五种:

image.png

解决线程安全问题

当多个线程操作共享数据的时候就会出现安全问题,解决的方法主要有以下几种:

  1. 同步代码块
synchronized(同步监视器){

    //需要被同步的代码;
}
复制代码

同步监视器即俗称的锁,任何一个类的对象都能充当锁,且多个线程得使用同一把锁。对于继承Thread方式的多线程,该锁需要设定为 类名.class;对于实现Runabble的方法的多线程,该锁可以为任意对象,例如Object obj

局限性:操作同步代码块时只能有一个线程参与,相当于变成了单线程,效率低下

  1. 同步方法

如果操作共享数据的代码都声明在同一个方法中,那么直接将该方法声明为同步方法

public synchronized void show(){//同步监视器this
    //需要被同步的代码;
}
复制代码

使用这种方式时,默认的同步监视器为this。对于继承Thread方式的多线程,需要将该同步方法再修饰为static,这样多个对象的同步监视器都相同即 类名.class;对于实现Runabble的方法的多线程,直接在run()中调用同步函数即可

  1. Lock锁

JDK中提供了LOCk接口,并且提供了其实现类Reentrantlock

class MyThread implements Runnable {
    private int tickets = 100;
    //1. 实例化Reentrant Lock对象
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //2. 调用lock()方法
                lock.lock();
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + " " + tickets);
                    tickets--;
                } else {
                    break;
                }
            } finally {
                //3. 调用解锁方法
                lock.unlock();
            }
        }
    }
}
复制代码

synchronized和lock的异同:

同:可以解决线程安全问题

异:synchronized在同步代码执行完之后自动释放同步监视器,lock需要手动的启动同步并释放同步

解决死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成了死锁

解决办法:

  • 专门的办法、原则
  • 尽量减少同步资源的定义
  • 尽量避免同步嵌套
线程的通信

wait():一旦执行此方法,当前线程就进入了阻塞状态并释放同步监视器,这样其他其他线程就能拿到同步监视器并执行同步方法

notify():一旦执行此方法,就会唤醒被wait的线程,如果有多个,则唤醒优先级高的那个线程

notifyAll():一旦执行此方法,就会唤醒所有被wait的线程

以上三个方法只能在同步方法或者同步代码块中调用,并且三个方法的调用者必须是同步代码块或者同步方法中的同步监视器

sleep和wait的异同

同:都会阻塞线程

异:1) 声明位置不同:sleep是在Thread类中声明的,wait是在Object类中声明的 2)调用范围不同:sleep可以在任何位置调用,而wait只能在同步代码块或者同步方法中调用 3) 当两者都运用在同步代码块或同步方法中时,sleep不会释放同步监视器,而wait会释放同步监视器