写在前面:这两天学习有点不太认真,这篇文章是根据b站上黑马视频整理出来的,还有很多不完善的地方,等过段时间再看老师视屏进行详细整理
8.13.开始重新写笔记,看了两遍视频,真不错
多线程
- 1.java是如何实现多任务的
- 2.java多线程编程
- 3.继承Thread类方式1:继承Thread类
- 4.实现Runable的接口
- 5.Callable和Future接口(jdk1.5)
- 6.线程对象的一些常用方法
- 7.✨继承Thread和实现Runable接口的区别?
- 8.同步锁
- 👀date8.9---
- 9.Lock锁
- 10.sychronized的锁升级
- 11.volatile关键字
- 12.synchronized在单例模式下的使用
- 13.线程的生命周期
- 14.✨死锁
- 👀date8.11---
- 15.原子类的使用
- 16.ThreadLocal
- 17.Java的对象引用
- 18.生产者和消费者模型
- 19.线程池
- 20.高并发下的几种常见容器的使用
1.java是如何实现多任务的
java是多线程实现多任务的!!!
java中是没有多进程和协程编程的!!!!
2.java多线程编程
|-- 继承Thread类
|-- 实现Runable的接口
|-- Callable和Future接口(jdk1.5)
|-- 线程池(jdk1.5)
3.继承Thread类方式1:继承Thread类
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
两个小问题:
- 为什么要重写run()方法?
- 因为run()是用来封装被线程执行的代码
- run()方法和start()方法的区别?
- run():封装线程执行的代码,直接调用,相当于普通方法的调用
- start():启动线程;然后由JVM调用此线程的run()方法
package com.study_01;
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
package com.study_01;
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
// my1.run();
// my2.run();
// void start()导致此线程开始执行; Java虚拟机调用此线程的run方法。
my1.start();
my2.start();
}
}
4.实现Runable的接口
方式2:实现Runnable接口
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
多线程的实现方案有两种
- 继承Thread类
- 实现Runnable接口
相比继承Thread类,实现Runnable接口的好处
- 避免了Java单继承的局限性
- 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想
package com.study_05;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
package com.study_05;
public class MyRunnableDemo {
public static void main(String[] args) {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
// 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
// Thread(Runnable target) 分配一个新的 Thread对象。
// Thread t1 = new Thread(my);
// Thread t2 = new Thread(my);
// Thread(Runnable target, String name)
Thread t1 = new Thread(my,"高铁");
Thread t2 = new Thread(my,"飞机");
//启动线程
t1.start();
t2.start();
}
}
5.Callable和Future接口(jdk1.5)
juc包是5.0
Callable、Future接口
Callable接口需要一个泛型,该泛型指的是线程方法执行完成后,需要返回的结果的类型;
Future接口中的TaskFuture实现类
该实现类,已经实现了Future和Runable接口
package com.study.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class TestThread03 {
public static void main(String[] args) {
// 创建线程对象 Callable线程对象
MyThread03 mt = new MyThread03();
// FutureTask 对象
FutureTask<String> future = new FutureTask<String>(mt);
// 启动了当前线程
new Thread(future, "call_thread").start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() +"主线程开始运行了"+ i);
}
// 注意:使用get方法获取,线程运行结束后的返回值
// 这个方法,意见在所有线程启动后,在调用
try {
System.out.println("子线程运行后得到的结果是:" + future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread03 implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
return "线程运行结束后的返回值";
}
}
6.线程对象的一些常用方法
线程对象的一些常见方法:
start() # 启动线程
run() # 该方法不能手动,是线程方法,start方法执行,JVM底层自动执行这个方法
setName(name) # 设置线程名称
getName() # 获取线程名称
getId() # 获取线程编号
getPriority() # 获取线程的优先级别
interrupt();
isAlive() # 判断线程是否存活
isDaemon() # 判断该线程是否是守护线程
setDaemon(true); # 将当前线程设置为守护线程
join(); # 阻塞主线程,让该子线程运行完成后再运行
Thread.sleep(毫秒) # 静态方法,当前线程休眠,会是否锁
Thread.yield() # 让当前线程放弃一次,不会释放锁
7.✨继承Thread和实现Runable接口的区别?
继承Thread类的子线程类,多个线程对象间是无法共享成员变量的!!!
如果是静态成员,也是共享的!!!!
实现Runable接口都子线程类,多个线程对象间是共享线程类的成员变量的!!!
8.同步锁
有三种写法
1、放在方法上:整个方法都是同步
如果某个方法中所有代码,都有可能出现线程安全问题,建议将synchronized
直接写在方法上面,如果将synchronized写在方法上面,该方法就是锁
2、同步块:将有可能出现线程安全的代码放在一个同步块中
key就是一个对象,什么对象都可以,一般建议使用this关键字充当
synchronized (key) {
// 可能出现线程安全问题
count++;
}
3、静态方法:比较特殊,单独说
该静态方法加synchronized关键字,因为静态方法属于类,所以该类(本质就是该类的字节码文件)在充当锁。
8.9号上课内容开始
👀date8.9—
9.Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达
如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
- void lock():获得锁
- void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
- Reentrantlock():创建一个ReentrantLock的实例
package com.study_11;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicket implements Runnable {
private int tickets = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tickets > 0) {
// 通过Sleep()方法类模拟卖票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
} finally {
lock.unlock();
}
}
}
}
package com.study_11;
public class SellTicketDemo {
public static void main(String[] args) {
// 创建SellTicket类的对象
SellTicket st = new SellTicket();
// 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
10.sychronized的锁升级
jdk7.0 synchronized进行了大的改动:
synchronized直接向操作系统申请锁,资源的消耗太大了,太重了。
Oracle进行了改造:
1、无锁状态(偏向锁)
2、锁就会从偏向锁升级到自旋锁(CAS)
CAS(compare and swap):比较并且交换
自旋锁很容易引起:ABA问题
3、自旋锁升级为重量级锁
CAS和乐观锁及悲观锁:
乐观锁的概念:始终任务线程不会产生并发问题
悲观锁:只要并发,就会产生线程安全问题
悲观锁:解决了并发问题,但是效率较差
乐观锁:不加锁,给数据添加一个版本号,每当值发生变化,就对版本号加1,最终通过比较版本,判断是否并发
11.volatile关键字
高并发:可见性、有序性、原子性
多线程情况下,每一个线程都独立拥有一个执行栈,每一个线程都是独立有用数据的,彼此之间是内存不可见的
在java中,volatile关键字有两大核心作用:
1、被它修饰的变量,在多线程中,可以打破内存屏障,也就是说被它修饰的变量,就有可见性的。
2、禁止指令重排序!!!
package com.study.thread;
public class TestThread03 {
public static void main(String[] args) {
MyThread03 mt = new MyThread03();
mt.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mt.flag = true;
System.out.println("count:"+mt.count);
}
}
class MyThread03 extends Thread {
public int count; // 0
public volatile boolean flag; // false
@Override
public void run() {
// public int count;
// 程序并没有被终止
while (!flag) {
count++;
}
}
}
12.synchronized在单例模式下的使用
gof 23中设计模式:
|-- 装饰器设计模式
|-- 单例设计模式
构造方法,肯定私有化的。
|-- 饿汉式
直接将对象在属性上创建,static,直接被内存,程序不退出,内存是不释放
这种非常好,没有线程安全问题,唯一的缺陷就是不管使用不使用,内存都要占据,而且不会释放
|-- 懒汉式
在需要使用对象的时候,再去创建对象,解决掉了饿汉式的内存占有问题。
懒汉式是无法直接使用的多线程中,因为非线程安全的.
如果要解决线程安全问题,需要加锁处理:
|-- 直接在方法上面加锁,直接解决掉了线程安全,但是效率不高(锁的范围太大)
|-- 在创建对象的代码上加锁(锁的范围小,效率高)
|-- 如果这种写法,需要两次判断非空
饿汉式
package com.study.thread;
public class Single {
// 在定义这个对象的时候,直接创建这个对象
private static Single single = new Single();
// 私有化构造函数
private Single() {}
public static Single getInstance() {
return single;
}
}
懒汉式
package com.study.thread;
public class Single2 {
// 在定义这个对象的时候,直接创建这个对象
// volatile 指令重排序
private static volatile Single2 single = null;
// 私有化构造函数
private Single2() {}
public static Single2 getInstance() {
if (single == null) {
single = new Single2();
}
return single;
}
}
package com.study.thread;
public class Test01 {
public static void main(String[] args) {
// Single s1 = new Single();
// Single s2 = new Single();
//
// System.out.println(s1 == s2);
// Single s = Single.getInstance();
// Single ss = Single.getInstance();
// // s 和 ss 就是一个对象
// System.out.println(s == ss);
Single2 single2 = Single2.getInstance();
Single2 single22 = Single2.getInstance();
System.out.println(single2 == single22 );
}
}
13.线程的生命周期
14.✨死锁
死锁(dead lock):
多线程情况下,线程安全问题,通过加锁来解决问题。
死锁现象:一定要避免!!!造成大量的资料浪费,同时有解决不了问题。
避免死锁:一种比较优秀的解决方案:银行家算法
形成死锁有四个必要条件:
|-- 互斥
|-- 请求保持
|-- 环路等待
|-- 不可剥夺条件
死锁是一种资源的浪费,在正常的编程中,一定要避免死锁
👀date8.11—
15.原子类的使用
java.util.concurrent.atomic这个包下的类,都是具有原子性的!!!
运算符:
自加 // i++ ==> t = i+1; i = t;
自减
+= // i += 3 ==> t = i + 3; i = t;
-=
不具备原子性,在多线程情况下,会出现线程安全问题
AtomicInteger这些类,所有方法都是使用自旋锁实现的具备原子性的方法
package com.study.atomic;
import java.util.concurrent.atomic.AtomicInteger;
public class TestAtomic {
public static void main(String[] args) {
MyThread01 mt1 = new MyThread01();
new Thread(mt1).start();
new Thread(mt1).start();
}
}
class MyThread01 implements Runnable {
// 一旦使用volatile修饰,虽然是可见,但是因为volatile不具备原子性
// 因此还是会出现线程安全问题
// private volatile int count;
private AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
// count++;
// 这个操作是使用自旋锁,具备原子性
count.incrementAndGet(); // ++count
// count.getAndIncrement(); // count++
// count.getAndAdd(10); // +=10
}
System.out.println(Thread.currentThread().getName()+":"+count);
}
}
16.ThreadLocal
线程对象提供了副本对象,特点是,每一个线程都独立拥有ThreadLocal对象
多线程情况下,一般比较喜欢使用ThreadLocal,多线程情况下,将值保存到
ThreadLocal中,多线程之间都是各自拥有各自的值,不会打架的
set() // 在自己线程中添加值
get() // 获取自己线程中存储在ThreadLocal中的值
remove() // 最后一定要移除值!!!,否则很容易出现内存溢出
package com.study.atomic;
public class TestThreadLocal {
// public static String name = "子线程";
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
// 该线程的threadLocal
System.out.println(threadLocal.get());
threadLocal.set("第一个线程的名字:张三");
printMsg(threadLocal);
// threadLocal添加的值,最后一定要移除掉,否则容易出现内存溢出
threadLocal.remove();
}
}).start();
// 使用lambda表达式完成
new Thread(() -> {
// 该线程的threadLocal
System.out.println(threadLocal.get());
threadLocal.set("另外一个线程的名字:李四");
printMsg(threadLocal);
// threadLocal添加的值,最后一定要移除掉,否则容易出现内存溢出
threadLocal.remove();
}).start();
}
private static void printMsg(ThreadLocal<String> threadLocal) {
System.out.println(Thread.currentThread().getName() + "---->" + threadLocal.get());
}
}
17.Java的对象引用
强引用:
Object o = new Object();
o就是一个强引用,不会被gc回收,即便是调用System.gc();
// o = null;
|-- 原子性:
java.util.concurrent.atomic包下的基于原子性的类
AtomicInteger
AtomicLong
AtomicBoolean
|-- ThreadLocal对象
每一个线程的私有对象
get()
set()
remove()
Thread使用的注意事项
|-- ThreadLocal底层源码
|-- java的
对象引用
|-- 强引用
|-- 软引用
|-- 弱引用
|-- 虚引用
18.生产者和消费者模型
生产者消费者模式
生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻所谓生产者消费
者问题,实际上主要是包含了两类线程:
- 一类是生产者线程用于生产数据
- 一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
- 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
- 消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Objec类中Object类的等待和唤醒方法:
生产者消费者案例
package com.study_12;
public class Box {
// 定义牛奶数量
private int milk;
// 定义一个成员变量,表示奶箱的状态
private boolean state = false;
// 提供存储牛奶的操作
public synchronized void put(int milk) {
// 如果有牛奶,等待消费
if (state) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有,就生产牛奶
this.milk = milk;
System.out.println("送奶工将第"+this.milk+"瓶奶放入奶箱");
// 生产完毕,修改奶箱状态
state = true;
// 唤醒
notifyAll();
}
public synchronized void get() {
// 如果没有,等待生产
if(!state) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果有就消费
System.out.println("用户拿到第"+this.milk+"瓶奶");
state = false;
// 唤醒
notifyAll();
}
}
package com.study_12;
public class Customer implements Runnable{
private Box b;
public Customer(Box b) {
this.b = b;
}
@Override
public void run() {
while (true){
b.get();
}
}
}
package com.study_12;
public class Producer implements Runnable{
private Box b;
public Producer(Box b) {
this.b = b;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
b.put(i);
}
}
}
package com.study_12;
public class BoxDemo {
public static void main(String[] args) {
// 创建奶箱队形,这是共享数据区域
Box b = new Box();
// 创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
Producer p = new Producer(b);
// 创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
Customer c = new Customer(b);
// 创建两个线程对象
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
// 启动线程
t1.start();
t2.start();
}
}
19.线程池
线程池:
池化模式:将大量我们需要的对象提前创建好,放在一个池(概念),
对象提前创建完成,也不需要销毁对象,所以说使用效率比较好
优点:使用效率比较好,避免对象的重复创建和销毁
缺点:内存占有较高,池的数量难以把控
池的数量的把控问题才是最关键的
java线程池是 1.5提供 juc包中,底层实现其实就是Callable和Future接口
|-- 根据情况,我们创建很多种不同场景的线程池
20.高并发下的几种常见容器的使用
高并发下的几种常见容器的使用
ArrayList
Vector
LinkedList
HashMap
HashSet
Hashtable
……
强调一个是:容器都存在着线程安全
|-- 线程安全的:
Vector:加了锁,好处:不存在线程安全问题,坏处:效率比较差
|-- juc包:
|-- ConrrentHashMap
|-- CopyOnWriteArrayList
|-- CopyOnWriteArraySet
|-- ConrrentHashMap
1、线程安全
2、读写分离