目录
1、认识线程(Thread)
1.1概念
1)线程是什么
一个线程就是一个执行流,每个线程之间都可以按照顺序执行自己的代码,多个线程之间同时执行着多份代码。
2)线程相比于进程的优势
创建线程比创建进程更快。
销毁线程比销毁进程更快。
调度线程比调度进程更快。
3)进程和线程的区别
进程是包含线程的,每个进程中至少包含一个线程(主线程)。
进程和进程是不共享同一个内存空间的,但同一个进程中的线程,是共享同一个内存空间的。
进程是资源分配的最小单元。线程是系统调度的最小单元。
4)java中线程与操作系统中线程的关系
java为了降低程序员的学习成本,将操作系统的API外嵌套了一层java自己的API,这样就可以让程序在不同的操作系统上运行,还不用更改程序。具体方式表示为使用Thread类
1.2创建第一个多线程
多线程的核心要领,就是在于 “同时运行”。
自定义一个类,然后继承Thread,同时复写其中的run方法。
class MyThread extends Thread{
@Override
public void run() {
System.out.println("执行该线程");
}
}
public class demo1{
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
myThread.run();
}
}
1.3使用Runnable接口方法创建线程
要注意由于Runnable是接口,不可创建对象,因此将其作为Thread的参数进行传入
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("线程使用成功");
}
}
public class demo2 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
其他变形
public class demo3 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("匿名内部类创建Thread子类对象");
}
};
t.start();
}
}
public class demo3 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类创建Runnable接口");
}
});
t.start();
}
}
public class demo3 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("匿名内部类创建lambda表达式");
});
t.start();
}
}
2、Thread类及其常见方法
2.1构造方法
可以采用空参构造、传入Runnable对象构造、传入线程名字构造
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2Thread的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
ID是线程的唯一表示。
名称是各种调试工具用到。
状态表示线程当前所处在的状态。
优先级高的理论上更容易被调度到。
JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活可以理解为run方法是否结束。
线程中断,后续说明。
2.3启动一个线程start
启动线程就必须要复写其中的run方法。
start()会调用其中的run方法来执行。若是直接使用t.run()则表示最普通的、调用对象t中的一个普通方法,并没有触发多线程。一定要通过start()去调用。
调用start方法,才真的在操作系统的底层创建出一个线程。
2.4中断一个线程
目前,有两种方法中断线程:
1.通过共享的标记来进行沟通。
2.调用interrupt()方法来通知。
public class demo4 {
public static boolean isInterrupt = false;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true){
if (!isInterrupt){
System.out.println("运行线程t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println("程序中断");
}
}
});
Thread i = new Thread(() -> {
try {
Thread.sleep(5000);
isInterrupt = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
i.start();
}
}
public class demo5 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
//当前线程是否发生中断
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
System.out.println("线程正常运行");
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
System.out.println("中断已经发生");
});
t.start();
Thread.sleep(5000);
t.interrupt(); //开始中断
}
}
Thread收到通知的方法主要有两种:
1.如果线程因为调用wait / join / sleep等方法而阻塞挂起,则以interruptedException异常的形式通知,清除中断标志。值得注意的是,当出现interruptedException的时候,要不要结束线程取决于catch中的写法。
2.如果catch中没有break,就只是内部的一个中断标志被设置,thread可以通过Thread.interrupted()判断当前线程的中断标志被设置,清除中断标志,Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志。
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
// 只有一开始是 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.interrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
// 全部是 true,因为标志位没有被清
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();
}
}
2.5等待一个线程
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("王五工作结束了");
}
}
2.6获取当前线程的引用
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
2.7休眠当前线程
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
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:这几个都表示排队等着其他事情。
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());
}
}
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();
}
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();
4、多线程带来的风险-线程安全(重点)
4.1发现线程不安全现象
static class Counter {
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);
}
4.2线程安全的概念
4.3线程不安全的原因
修改共享数据
上面线程不安全的代码中,涉及多个线程针对counter.count变量进行修改。
此时这个counter.count是一个多线程都能访问到的 “共享数据”。
counter.count变量是在堆上的,因此可以被多个线程访问到。
原子性
=>线程之间的共享变量存在主内存(main memory)
=>每一个线程都有自己的工作内存(working memory)
=>当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据
=>当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
之所以这么大费周章的划分这么多内存,原因有二:
代码重排序是指JVM会保持逻辑不变的情况下,对代码执行顺序进行调整,方便更快运行,但此时可能会使得代码出现bug。
4.4解决之前的线程不安全问题
其实只需要把increase这个方法加上synchronized锁,锁住即可,即表示当我执行该方法时,其他线程就不可以操作该方法了。
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
}
5、synchronized关键字
5.1 synchronized 的特性
1)互斥性
3)可重入(锁套娃)
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
=>如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
=>解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
5.2synchronized使用示例
1)直接修饰普通方法
public class SynchronizedDemo {
public synchronized void methond() {
}
}
2)修饰静态方法
public class SynchronizedDemo {
public synchronized static void method() {
}
}
3)修饰代码块
//锁当前对象,this
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
//锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
6、volatile关键字
举例说明:
实际上就是可以理解为,如果我不加volatile,那么当我在运行线程1的时候,线程2过来插了一脚,线程2把值修改了,但此时线程1还在运行,此时就会产生bug,我的理想情况应该是线程2一修改,线程1就停止。当我加上volatile以后,线程2一变,线程1就停了。
原因就是因为(通俗理解)程序是从内存中取出flag值存在一个临时内存上,线程2修改的是原内存上的,临时内存并没有变。
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 线程循环能够立即结束.
//此时可以看到, 最终 count 的值仍然无法保证是 100000.
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);
}
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();
}
7、wait和notify
使用wait()让某些线程阻塞,使用notify()唤醒某些阻塞线程。通过这种方式来调节线程执行顺序。
7.1wait()方法
wait()必须搭配synchronized使用,若未使用synchronized则会爆出异常。
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
7.2notify()方法
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
7.3notifyAll() 方法
//notifyAll()唤醒所有线程
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notifyAll();
System.out.println("notify 结束");
}
}
7.4wait和sleep的对比
其实没有可比性,一个用于线程之间通信的,一个让线程阻塞一段时间。
唯一相同点就是都让程序阻塞一段时间。
8、多线程案例
8.1单例模式
单例模式分为饿汉和懒汉模式。
饿汉与懒汉的区别,就在于实例创建的时机不同。
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
注意理解这里两个if的使用:线程加锁开锁是意见开销比较大的事情。而懒汉模式的线程不安全,仅仅只存在于第一次创建实例的时候,后续操作都不需要进行加锁。因此,此处第一个if就是为了判断是否已经把instance实例创建出来了。
也就是说,外面的if是为了避免反复执行里面的synchronized。里面的if是正常逻辑,即有实例就不创建,没有实例就创建。
8.2阻塞式队列
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take()
生产消费者模型
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int num = random.nextInt(1000);
System.out.println("生产元素: " + num);
blockingQueue.put(num);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者");
producer.start();
customer.join();
producer.join();
}
8.3定时器
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
8.4线程池
把创建的线程都放入一个池子里
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});