多线程(基础)

1.认识线程

1.1线程是什么

一个线程就是一个 " 执行流 ", 每个线程之间都可以按照顺讯执行自己的代码, 多个线程之间 " 同时 " 执行着多份代码.

1.2为什么要用线程

其一,并发编程成为刚需。单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源,有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.

其二,虽然多进程也能实现并发编程, 但是线程比进程更轻量.

创建线程比创建进程更快.

销毁线程比销毁进程更快.

调度线程比调度进程更快.

其三,线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) "协程"

(Coroutine)

1.3进程与线程的区别

1)进程包含线程

每一个进程至少有一个线程存在,即主线程。

2)进程与进程之间是不共享内存空间的,而同一个进程的线程之间是共享内存空间的。

3)进程是系统分配资源的最小单位,线程是系统调度的最小单位

1.4第一个多线程程序

多线程程序与普通程序的区别在于:每个线程都是一个独立的执行流,多个线程之间是并发执行的

import java.util.Random;
public class ThreadDemo {
    private static class MyThread extends Thread {
        @Override
        public void run() {
            Random random = new Random();
            while (true) {
                // 打印线程名称
                System.out.println(Thread.currentThread().getName());
                try {
                    // 随机停止运行 0-9 秒
使用 jconsole 命令观察线程
                    Thread.sleep(random.nextInt(10));
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }
   }
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
        Random random = new Random();
        while (true) {
            // 打印线程名称
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(random.nextInt(10));
           } catch (InterruptedException e) {
                // 随机停止运行 0-9 秒
                e.printStackTrace();
           }
       }
   }
}

1.5创建线程(重点)

方法一:继承Thread类

class MyThread extends Thread{

    //继承以后,要将父类中的方法重写
    @Override
    public void run() {
        //线程的入口方法
        while (true){
        System.out.println("hello thread");
        //这个地方只能用try catch,因为父类的方法没有throws,所以子类的也没法throws
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}


public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        //start和run都是thread的成员。
        //run只是描述了线程的入口(线程要做什么任务)
        //start则是真正调用了系统API,在系统中创建出线程,让线程再调用run
        //t.run();

        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(3000);
        }

    }
}

方法二:实现Runnable接口

class MyRunnable implements Runnable{

    @Override
    public void run() {
        while(true) {
            System.out.println("Hello MyRunnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}


public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        //Runnable表示一个可以运行的任务,这个任务是交给线程负责执行,还是交给其他实体来执行
        //Runnable本身并不关心
        //终究还是通过t.start来调用系统api来完成创建线程的工作
        Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        //Thread t = new Thread(new MyRunnable()); //等价写法
        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }

    }
}

对比以上两种方法:

继承Thread类,直接使用this就表示当前线程对象的引用。

实现Runnable接口,this 表示的是 MyRunnable 的引用需要使用 Thread.currentThread()

方法三:匿名内部类创建Thread子类对象

public class demo3 {
    public static void main(String[] args) throws InterruptedException {

        Thread t =new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }

    }
}

方法四:匿名内部类创建Runnable子类对象

public class demo4 {
    public static void main(String[] args) throws InterruptedException {
//        Runnable runnable = new Runnable() {
//            @Override
//            public void run() {
//                while (true) {
//                    System.out.println("hello");
//                    try {
//                        Thread.sleep(1000);
//                    } catch (InterruptedException e) {
//                        throw new RuntimeException(e);
//                    }
//                }
//            }
//        };

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();

        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

方法五:lambda表达式创建Runnable子类对象(最推荐)

public class demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("hello lambda");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }

    }
}

2.Thread类及常见方法

2.1Thread的常见构造方法

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
属性获取方法
IDgetld()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

ID是线程的唯一标识,不同线程不会重复。

名称是各种调试工具可以用到

状态表示线程当前所处的一个情况,下面我们会进一步说明
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住一点: JVM 会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活,即简单的理解,为 run 方法是否运行结束了
线程的中断问题,下面我们进一步说明

2.2启动一个线程

start启动,重写run只是相当于修改了run的内容,只有使用t.start()方法,才是真正执行线程。

2.3中断一个线程

目前常见的有以下两种方式:
1. 通过共享的标记来进行沟通
2. 调用 interrupt() 方法来通知
第一种如下
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;
        @Override
        public void run() {
            while (!isQuit) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
       }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        target.isQuit = true;
   }
}

 第二种如下(推荐)

public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 两种方法均可以
            while (!Thread.interrupted()) {
            //while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()
                            + ": 有内鬼,终止交易!");
                    // 注意此处的 break
                    break;
               }
           }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
       }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        thread.interrupt();
   }
}

注意区分Thread.interrupted()Thread.currentThread().isInterrupted(),可以理解为是否需要归位。比如变为true或false以后,后续操作是继续保持当前的true/false还是归位成初始的true/false。

//使用interrupt会清楚标志位
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.interrupted());
           }
       }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
   }
}
// 只有一开始是 true,后边都是 false,因为标志位被清
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
 System.out.println(Thread.currentThread().isInterrupted());
           }
       }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
   }
}
// 全部是 true,因为标志位没有被清

2.4等待一个进程join()

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() 
                                       + ": 我还在工作!");
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
       };
        Thread thread1 = new Thread(target, "李四");
        Thread thread2 = new Thread(target, "王五");
        System.out.println("先让李四开始工作");
        thread1.start();
        thread1.join();
        System.out.println("李四工作结束了,让王五开始工作");
        thread2.start();
        thread2.join();
        System.out.println("王五工作结束了");
   }
}

3.线程的状态

3.1观察线程的所有状态

public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
}
NEW: 安排了工作 , 还未开始行动
RUNNABLE: 可工作的 . 又可以分成正在工作中和即将开始工作 .
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了

3.2观察线程状态和转移

观察1:关注NEW、RUNNABLE、TERMINATED的状态的转换

//使用isAlive()来判断线程是否活着
public class ThreadStateTransfer {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 1000_0000; i++) {
           }
       }, "李四");
        System.out.println(t.getName() + ": " + t.getState());
        t.start();
        while (t.isAlive()) {
            System.out.println(t.getName() + ": " + t.getState());
       }
        System.out.println(t.getName() + ": " + t.getState());
   }
}

观察2:关注WATTING、BLOCKED、TIME_WAITTING状态的转换

public static void main(String[] args) {
    final Object object = new Object();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (object) {
                while (true) {
                    try {
                        Thread.sleep(1000);
                   } catch (InterruptedException e) {
                        e.printStackTrace();
                   }
               }
           }
       }
   }, "t1");
    t1.start();
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println("hehe");
           }
       }
   }, "t2");
    t2.start();
}
结论 :
BLOCKED 表示等待获取锁 , WAITING TIMED_WAITING 表示等待其他线程发来通知 .
TIMED_WAITING 线程在等待唤醒,但设置了时限 ; WAITING 线程在无限等待唤醒
观察3:yield()让出CPU资源
Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            System.out.println("张三");
            // 先注释掉, 再放开
            // Thread.yield();
       }
   }
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            System.out.println("李四");
       }
   }
}, "t2");
t2.start();
可以看到 :
1. 不使用 yield 的时候 , 张三李四大概五五开
2. 使用 yield , 张三的数量远远少于李四
结论 :
yield 不改变线程的状态 , 但是会重新去排队

4.多线程的安全问题(重点)

4.1现象

多线程存在安全问题,主要是各个线程中指令交叉执行。

//线程安全
public class demo11 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            //对count变量自增5w次
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            //对count变量自增5w次
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();

        t1.join();
        //如果没有join,线程还没执行完就开始打印了
        t2.join();

        //预期结果应该是10w,但是此时结果与10w相差甚远
        System.out.println("count:" + count);

    }
}

4.2原因

上面的线程不安全的代码中 , 涉及到多个线程针对 counter.count 变量进行修改 .
此时这个 counter.count 是一个多个线程都能访问到的 " 共享数据”

4.3解决线程不安全的方法

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

4.4synchronized 关键字

synchronized有以下特性

1)互斥

synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到
同一个对象 synchronized 就会 阻塞等待 .
进入 synchronized 修饰的代码块 , 相当于 加锁
退出 synchronized 修饰的代码块 , 相当于 解锁
synchronized 的底层是使用操作系统的 mutex lock 实现的 .

2)可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
即一个线程没有释放锁,然后又尝试再次加锁。
在下面的代码中 ,
increase increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前
对象加锁的 .
在调用 increase2 的时候 , 先加了一次锁 , 执行到 increase 的时候 , 又加了一次锁 . ( 上个锁还没释
, 相当于连续加两次锁 )
这个代码是完全没问题的 . 因为 synchronized 是可重入锁 .
static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}
在可重入锁的内部 , 包含了 " 线程持有者 " " 计数器 " 两个信息 .
如果某个线程加锁的时候 , 发现锁已经被人占用 , 但是恰好占用的正是自己 , 那么仍然可以继续获取
到锁 , 并让计数器自增 .
解锁的时候计数器递减为 0 的时候 , 才真正释放锁 . ( 才能被别的线程获取到)

4.5synchronized使用示例

1)直接修饰普通方法。

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}

2)修饰静态方法。

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

3)修饰代码块。

//锁当前对象
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}
//锁类对象
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}
我们重点要理解, synchronized 锁的是什么 . 两个线程竞争同一把锁 , 才会产生阻塞等待 .

5.volatile关键字

volatile 修饰的变量 , 能够保证 " 内存可见性 ".
代码在写入 volatile 修饰的变量的时候 ,
改变线程工作内存中 volatile 变量副本的值
将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候 ,
从主内存中读取 volatile 变量的最新值到线程的工作内存中
从工作内存中读取 volatile 变量的副本

前面我们讨论内存可见性时说了 , 直接访问工作内存 ( 实际是 CPU 的寄存器或者 CPU 的缓存 ), 速度
非常快 , 但是可能出现数据不一致的情况 .
加上 volatile , 强制读写内存 . 速度是慢了 , 但是数据变的更准确了 .

代码示例
在这个代码中
创建两个线程 t1 t2
t1 中包含一个循环 , 这个循环以 flag == 0 为循环条件 .
t2 中从键盘读入一个整数 , 并把这个整数赋值给 flag.
预期当用户输入非 0 的值的时候 , t1 线程结束 .

static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (counter.flag == 0) {
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
static class Counter {
    public volatile int flag = 0;
}
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.
volatile 不保证原子性
volatile synchronized 有着本质的区别 . synchronized 能够保证原子性 , volatile 保证的是内存可见性.
代码示例
这个是最初的演示线程安全的代码 .
increase 方法去掉 synchronized
count 加上 volatile 关键字 .
static class Counter {
    volatile public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}
此时可以看到 , 最终 count 的值仍然无法保证是 100000.
synchronized 也能保证内存可见性
synchronized 既能保证原子性 , 也能保证内存可见性 .
对上面的代码进行调整 :
去掉 flag volatile
t1 的循环内部加上 synchronized, 并借助 counter 对象加锁 .
static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (true) {
            synchronized (counter) {
                if (counter.flag != 0) {
                    break;
               }
           }
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值