Java 多线程的使用

多线程的使用

一,继承Thread类

构造方法:
方法名 说明
Thread() 分配新的Thread对象。
Thread(String name) 分配新的Thread对象,将指定的name作为其线程名称。
常用方法:
方法名 说明
void start() 使该线程开始执行:Java虚拟机调用该线程的run方法。
void run() 该线程要执行的操作。
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
创建线程的步骤:
  1. 定义一个类继承Thread类。
  2. 重写run方法。
  3. 创建子类对象,就是创建线程对象。
  4. 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
public class Demo {
    public static void main(String[] args) {
        //创建自定义线程对象
        MyThread mt = new MyThread("新的线程!");
        //开启新线程
        mt.start();
        //在主方法中执行for循环
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程!" + i);
        }
    }
}

//线程类
class MyThread extends Thread {
    //定义指定线程名称的构造方法
    public MyThread(String name) {
        //调用父类的String参数的构造方法,指定线程的名称
        super(name);
    }

    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + ":正在执行!" + i);
        }
    }
}

线程对象调用 run方法和调用start方法区别?线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。

获取线程名称
方法名 说明
Thread.currentThread() 获取当前线程对象
Thread.currentThread().getName(); 获取当前线程对象的名称

二,实现Runnable接口

接口中的方法:
方法名 说明
void run() 使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行中的线程调用对象的run方法。
创建线程的的步骤:
  1. 定义类实现Runnable接口。
  2. 覆盖接口中的Run方法。
  3. 创建Thread类的对象。
  4. 将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
  5. 调用Thread类的start方法开启线程。
public class Demo {
    public static void main(String[] args) {
//创建线程执行目标类对象
        Runnable runn = new MyRunnable();
//将Runnable接口的子类对象作为参数传递给Thread类的构造函数
        Thread thread = new Thread(runn);
        Thread thread2 = new Thread(runn);
//开启线程
        thread.start();
        thread2.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程:正在执行!" + i);
        }
    }
}

//自定义线程执行任务类
class MyRunnable implements Runnable {
    //定义线程要执行的run方法逻辑
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我的线程:正在执行!" + i);
        }
    }
}

线程池的使用方法

线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

我们详细的解释一下为什么要使用线程池?

在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

一,使用Runnable接口

通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。

Executors:线程池创建工厂类
方法名 说明
public static ExecutorService newFixedThreadPool(int nThreads) 返回线程池对象
ExecutorService:线程池类
方法名 说明
Future<?> submit(Runnable task) 获取线程池中的某一个线程对象,并执行
Future接口 用来记录线程任务执行完毕后产生的结果。线程池创建与使用
使用线程池中线程对象的步骤:
  1. 创建线程池对象
  2. 创建Runnable接口子类对象
  3. 提交Runnable接口子类对象
  4. 关闭线程池
package thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
		//创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
		//创建Runnable实例对象
        MyRunnable r = new MyRunnable();

		//自己创建线程对象的方式
		//Thread t = new Thread(r);
		//t.start(); ---> 调用MyRunnable中的run()

		//从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
		//再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
		//注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中

		//关闭线程池
		//service.shutdown();
    }
}

//Runnable接口实现类
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一个教练");

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教练回到了游泳池");
    }
}

二,使用Callable接口

Callable接口:与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。

ExecutorService:线程池类

方法名 说明
Future submit(Callable task) 获取线程池中的某一个线程对象,并执行线程中的call()方法
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用
  1. 使用线程池中线程对象的步骤:
  2. 创建线程池对象
  3. 创建Callable接口子类对象
  4. 提交Callable接口子类对象
  5. 关闭线程池
package cn.last.demo;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SpringTestApplicationTests {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        //创建Callable对象
        MyCallable c = new MyCallable();

        //从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(c);

        //再获取个教练
        service.submit(c);
        service.submit(c);
        //注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中

        //关闭线程池
        //service.shutdown();
    }
}

//Callable接口实现类,call方法可抛出异常、返回线程任务执行完毕后的结果
class MyCallable implements Callable<Object> {
    @Override
    public Object call() throws Exception {
        System.out.println("我要一个教练:call");
        Thread.sleep(2000);
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教练回到了游泳池");
        return null;
    }
}

三,线程池练习:返回两个数相加的结果

要求:通过线程池中的线程对象,使用Callable接口完成两个数求和操作

  • Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用
  • V get() 获取Future对象中封装的数据结果
package cn.last.demo;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class SpringTestApplicationTests {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(2);

        //创建一个Callable接口子类对象
        //MyCallable c = new MyCallable();
        MyCallable c = new MyCallable(100, 200);
        MyCallable c2 = new MyCallable(10, 20);

        //获取线程池中的线程,调用Callable接口子类对象中的call()方法, 完成求和操作
        //<Integer> Future<Integer> submit(Callable<Integer> task)
        // Future 结果对象
        Future<Integer> result = threadPool.submit(c);
        //此 Future 的 get 方法所返回的结果类型
        Integer sum = result.get();
        System.out.println("sum=" + sum);

        //再演示
        result = threadPool.submit(c2);
        sum = result.get();
        System.out.println("sum=" + sum);
        //关闭线程池(可以不关闭)

    }
}

//Callable接口实现类
class MyCallable implements Callable<Integer> {
    //成员变量
    int x;
    int y;

    public MyCallable(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public Integer call() {
        return x + y;
    }
}

线程同步(线程安全处理Synchronized)

java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

一,同步代码块

同步代码块: 在代码块声明上 加上synchronized

synchronized (锁对象) {
    可能会产生线程安全问题的代码
}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:

package cn.last.demo;

//测试类
public class SpringTestApplicationTests {
    public static void main(String[] args) {
        //创建票对象
        Ticket ticket = new Ticket();
        //创建3个窗口
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

//模拟票
class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定义锁对象
    Object lock = new Object();

    @Override
    public void run() {
        //模拟卖票
        while (true) {
            //同步代码块
            synchronized (lock) {
                if (ticket > 0) {
                    //模拟电影选坐的操作
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
                }
            }
        }
    }
}

二,同步方法

同步方法:在方法声明上加上synchronized

public synchronized void method(){
       可能会产生线程安全问题的代码
}

同步方法中的锁对象是 this

使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:

三,静态同步方法: 在方法声明上加上static synchronized

public static synchronized void method(){
    可能会产生线程安全问题的代码
}

静态同步方法中的锁对象是 类名.class

四,死锁

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

synchronzied(A锁){
    synchronized(B锁){
    }
}

我们进行下死锁情况的代码演示:

package cn.last.demo;

import java.util.Random;

//定义锁对象类
class MyLock {
    public static final Object lockA = new Object();
    public static final Object lockB = new Object();
}

//测试类
public class SpringTestApplicationTests {
    public static void main(String[] args) {
        //创建线程任务类对象
        ThreadTask task = new ThreadTask();
        //创建两个线程
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        //启动线程
        t1.start();
        t2.start();
    }
}

//线程任务类
class ThreadTask implements Runnable {
    int x = new Random().nextInt(1);//0,1

    //指定线程要执行的任务代码
    @Override
    public void run() {
        while (true) {
            if (x % 2 == 0) {
                //情况一
                synchronized (MyLock.lockA) {
                    System.out.println("if-LockA");
                    synchronized (MyLock.lockB) {
                        System.out.println("if-LockB");
                        System.out.println("if大口吃肉");
                    }
                }
            } else {
                //情况二
                synchronized (MyLock.lockB) {
                    System.out.println("else-LockB");
                    synchronized (MyLock.lockA) {
                        System.out.println("else-LockA");
                        System.out.println("else大口吃肉");
                    }
                }
            }
            x++;
        }
    }
}

五,Lock接口

查阅API,查阅Lock接口描述,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。

Lock接口中的常用方法:

方法名 说明
void lock() 获取锁
void unlock() 释放锁

Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。

我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:

package cn.last.demo;

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

class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //创建Lock锁对象
    Lock ck = new ReentrantLock();

    @Override
    public void run() {
        //模拟卖票
        while (true) {
            //synchronized (lock){
            ck.lock();
            if (ticket > 0) {
                //模拟选坐的操作
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
            }
            ck.unlock();
        }
    }
}

六,等待唤醒机制

在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

等待唤醒机制所涉及到的方法:

方法名 说明
void notify() 唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
void notifyAll() 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
void wait() 等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。

其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。

仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?

因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。

原文地址:https://www.cnblogs.com/SnowPrince/p/14385743.html