多线程

线程与进程

  • 进程是程序的一次动态执行,是系统进行资源分配和调度的基本单位,是操作系统运行的基础,通常每一个进程都拥有自己独立的内存空间和系统资源
  • 线程是程序运行的执行单元,依托于进程存在。一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源

多进程
特点:内存隔离,单个进程的异常不会导致整个应用的崩溃,方便调试;但是进程间调用、通信和切换的开销大
常使用在目标子功能间交互少的场景,弱相关性的、可扩展到多机分布(Nginx 负载均衡)的场景
多线程
特定:提高了系统的并行性,开销小;但是没有内存隔离,单个线程崩溃会导致整个应用的退出,定位不方便
常使用在存在大量 I/O,网络等耗时操作,或者需要与用户进行交互,频繁创建和销毁的 Web 服务、大量计算的、强相关性的、多核分布(多核 CPU)的场景

线程的状态

  • 新建状态(New)
    当用 new 操作符创建一个线程时, 例如 new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新建状态时,程序还没有开始运行线程中的代码。

  • 就绪状态(Runnable)
    一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的 start() 方法。当线程对象调用 start() 方法即启动了线程,start() 方法创建线程运行的系统资源,并调度线程运行 run() 方法。当 start() 方法返回后,线程就处于就绪状态。
    处于就绪状态的线程并不一定立即运行 run() 方法,线程还必须同其他线程竞争 CPU 时间,只有获得 CPU 时间才可以运行线程。因为在单 CPU 的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。

  • 运行状态(Running)
    当线程获得 CPU 时间后,它才进入运行状态,真正开始执行 run() 方法

  • 阻塞状态(Blocke)
    阻塞状态是正在运行的线程没有运行结束,暂时让出 CPU,这时其他处于就绪状态的线程就可以获得 CPU 时间,进入运行状态。线程运行过程中,可能因为各种原因进入阻塞状态:

    • 线程通过调用 sleep 方法进入睡眠状态
    • 线程调用一个在 I/O 上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者
    • 线程试图得到一个锁,而该锁正被其他线程持有
    • 线程在等待某个触发条件
    • ......
  • 死亡状态(Dead)
    有两个原因会导致线程死亡:

    • run方法正常退出而死亡
    • 某个未捕获的异常终止了 run 方法而使线程猝死

    为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用 isAlive 方法。如果是可运行或被阻塞,这个方法返回 true; 如果线程仍旧是 new 状态且不是可运行的, 或者线程死亡了,则返回 false

线程的创建方式

继承 Thread 类

/**
 * 如果调用run()方法,程序会先执行run()方法,然后再去执行主路径main方法
 * 如果调用start()方法,主线程main方法和run()方法会并行交替执行
 *
 *线程开启不一定立即执行,有CPU调度执行
 */

//创建线程的方式一:继承Thread类,重写run()方法,调用start方法开启线程
public class TestThread1 extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("Thread学习---");
        }
    }

    //main也是一个线程
    public static void main(String[] args) {
        //创建一个线程对象
        TestThread1 testThread1 = new TestThread1();

        //调用start()方法开启线程
        testThread1.start();
        //testThrea1.run();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main方法---" + i);
        }
    }
}

输出部分截图:

输出部分截图

可以发现,当调用start方法时,主线程main和run()会交替执行

如果调用run()方法,会先执行run(),执行完毕后再执行主线程main

实现 Runnable 接口

//创建线程方式2:实现Runnable接口,重写run()方法,执行线程需要丢入Runnable接口的实现类。调用start()
public class TestThread2 implements Runnable{
    private String name;
    public TestThread2(String name){
        this.name = name;
    }

    @Override
    public void run() {
        //run方法线程体
            System.out.println("线程名---" + name);
    }

    public static void main(String[] args) {

        TestThread2 threadA = new TestThread2("线程A");
        TestThread2 threadB = new TestThread2("线程B");
        TestThread2 threadC = new TestThread2("线程C");

        new Thread(threadA).start();
        new Thread(threadB).start();
        new Thread(threadC).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main方法---" + i);
        }
    }
}

输出部分截图:


输出部分截图

与继承Thread不同
实现Runnable接口在调用start()方法时

//需要先创建Runnable接口的实现类
TestThread2 testThread2 = new TestThread2();
//然后再创建线程对象,调用start()方法
Thread thread2 = new Thread(testThread2);
thread2.start();

//或者可以简写
TestThread2 testThread2 = new TestThread2();
new Thread(testThread2).start();

第一种方法直接继承 Thread 类,其实 Thread 也实现了 Runnable 接口

实现 Callable 接口

  1. 实现 Callable 接口
  2. 重写 call 方法,需要返回值类型
  3. main方法需要抛出异常
import java.util.concurrent.*;

//线程创建方式3:实现Callable接口
public class TestThread3 implements Callable {
    @Override
    public Boolean call() {
        //重写call方法,需要返回值
        for (int i = 0; i < 20; i++) {
            System.out.println("Thread学习---");
        }

        return true;
    }

    //main方法需要抛出异常
    public static void main(String[] args) throws Exception {

        TestThread3 t1 = new TestThread3();

        FutureTask<Boolean> result1 = new FutureTask(t1);
        new Thread(result1).start();

        //返回值
        System.out.println(result1.get());
    }
}

输出部分截图:


输出部分截图

多线程——龟兔赛跑

public class Race implements Runnable {

    //胜利者
    private static String winner;

    //重写run()方法
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {

            //模拟兔子休息,每跑10布休息一会
            if (Thread.currentThread().getName().equals("兔子") && (i%10 == 0) && (i != 0) ){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //判断比赛是否结束
            boolean flag = gameOver(i);
            //如果比赛结束,则停止程序
            if (flag) {
                break;
            }

            System.out.println(Thread.currentThread().getName() + "-->跑了" + i);
        }
    }

    //判断是否完成比赛
    private boolean gameOver(int steps) {
        //判断是否有胜利者
        if (winner != null) {
            return true;
        } else if (steps >= 100) {
            winner = Thread.currentThread().getName();
            System.out.println("winner is" + winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race,"兔子").start();
        new Thread(race,"乌龟").start();
    }
}

模拟多人抢票

//多个线程同时操作同一个对象
//买火车票的例子

//多个线程操作同一个资源的情况下,线程不安全,数据紊乱

public class TestThread4 implements Runnable{

    //票数
    private int ticketNUm = 10;
    boolean flag = true;

    @Override
    public void run() {
        //买票
        while (flag){
            buy();
        }
    }

    private void buy(){
        //判断是否有票
        if(ticketNUm == 0){
            flag = false;
            return;
        }

        //模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //买票
        System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNUm-- + "张票");
    }

    public static void main(String[] args) {

        TestThread4 testThread4 = new TestThread4();

        new Thread(testThread4,"学生").start();
        new Thread(testThread4,"老师").start();
        new Thread(testThread4,"黄牛").start();

    }
}
输出紊乱,出现了第0张和第-1张票

解决办法:

//在写操作票数量的代码buy()方法时,加锁sychronized
private synchronized void buy(){...}

利用多线程向数据库插入10万条数据

首先创建数据库表tb_demo
tb_demo

数据库连接

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Vector;

public class JdbcUtils {
    static String driver = "com.mysql.cj.jdbc.Driver";
    static String url="jdbc:mysql://localhost:3306/thread?serverTimezone=UTC";
    static String user="root";
    static String pwd = "666666";

    static Vector<Connection> pools = new Vector<Connection>();

    public static Connection getDBConnection(){
        try {
            //1.加载驱动
            Class.forName(driver);
            //2.取得数据库连接
            Connection conn =  DriverManager.getConnection(url, user, pwd);
            return conn;
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

    static {
        int i = 0;
        while(i<10){
            pools.add(getDBConnection());
            i++;
        }
    }

    public static synchronized Connection getPool(){
        if(pools != null && pools.size() > 0){
            int last_ind = pools.size() -1;
            return pools.remove(last_ind);
        }else{
            return getDBConnection();
        }
    }

    public static int insert(String sql,Object[] params){
        Connection conn = getPool();
        PreparedStatement pstmt = null;
        try {
            pstmt = conn.prepareStatement(sql);
            for(int i=0;i<params.length;i++){
                pstmt.setObject(i+1, params[i]);
            }
            return pstmt.executeUpdate();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            if(pstmt != null){
                try {
                    pstmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            if(conn != null){
                pools.add(conn);
            }
        }
        return 0;
    }
}

10个线程向数据库插入10万条数据

import java.util.Date;
import java.util.concurrent.CountDownLatch;

public class doInsert {
    public static void main(String[] args) {
        long startTimes = System.currentTimeMillis();
        String[] names = new String[]{"A","B","C","D","E","F","G","H","I","J"};
        int threadCount = 10;
        int total = 100000;
        int every = total/threadCount;
        final CountDownLatch latch = new CountDownLatch(threadCount);
        for(int i=0;i<threadCount;i++){
            new Thread(new Worker(latch,names[i],i*every,(i+1)*every)).start();
        }
        try {
            latch.await();
            long endTimes = System.currentTimeMillis();
            System.out.println("所有线程执行完毕:" + (endTimes - startTimes));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Worker implements Runnable{

    static String sql = "INSERT INTO `tb_demo` (`name`,cre_date) VALUES (?, ?);";
    int start = 0;
    int end = 0;
    String name = "";
    CountDownLatch latch;
    public Worker(CountDownLatch latch,String name, int start,int end){
        this.start = start;
        this.end = end;
        this.name = name;
        this.latch = latch;
    }

    @Override
    public void run() {
        for (int i = start; i < end; i++) {
            System.out.println("线程" + Thread.currentThread().getName()+ "正在执行。。");
            Object[] params = new Object[] { name + i, new Date() };
            JdbcUtils.insert(sql, params);
        }
        latch.countDown();
    }

}

执行结果

10个线程大约需要85秒
数据库表中的部分数据

而如果用一个线程来插入这10万条数据,将线程数 threadCount 设为1.
则需要396秒大约6分半钟!!


一个线程需要300多秒
数据库表中的部分数据
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。