1. 核心概念
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程, 后台也会有多个线程, 如主线程,gc线程;
- main() 称之为主线程, 为 系统的入口, 用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行有调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为干预的;
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,如cpu调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
2. 创建线程的三种方式
2.1 继承Thread类
继承Thread类,重写run()方法编写线程执行体,调用start()开启线程;
注: 线程不一定立即执行
public class Demo1 extends Thread{
@Override
public void run() {
for (int i=0;i<200;i++){
System.out.println("我在看代码==========="+i);
}
}
public static void main(String[] args) {
Demo1 t = new Demo1();
t.start();
for (int i=0;i<1000;i++){
System.out.println("我在学习多线程========="+i);
}
}
}
2.2 实现Runnable接口
1.定义一个类实现Runnable接口;
2.实现run()方法,编写线程执行体
3.创建线程对象,调用start()方法启动线程 //new Thread(xx).start();
public class TestThread2 implements Runnable {
@Override
public void run() {
for (int i=0;i<200;i++){
System.out.println("我在看代码==========="+i);
}
}
public static void main(String[] args) {
TestThread2 testThread2 = new TestThread2();
new Thread(testThread2).start();
for (int i=0;i<1000;i++){
System.out.println("我在学习多线程========="+i);
}
}
}
实现Callable接口
- 实现Callable接口, 需要返回值类型
- 重写call方法,需要抛异常
- 创建目标对象
- 创建执行服务: ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行: Future result1 = ser.submit(t1);
- 获取结果: boolean r1 = result1.get();
- 关闭服务: ser.shutdownNow();
3.静态代理
静态代理模式总结:
1.真实对象和代理对象都要实现同一个接口;
2.代理对象要代理真实角色
好处:
1 代理对象可以做很多真实对象做不了的事情;
2 真实对象专注做自己的事情
public class StaticProxy {
public static void main(String[] args) {
// WeddingCompany weddingCompany = new WeddingCompany(new You());
// weddingCompany.HappyMarry();
new WeddingCompany(new You()).HappyMarry();
}
}
interface Marry{
void HappyMarry();
}
//真实角色,你去结婚
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("xx结婚了...");
}
}
//代理角色, 帮助你结婚
class WeddingCompany implements Marry{
//目标对象
private Marry target;
public WeddingCompany(Marry target) {
this.target = target;
}
@Override
public void HappyMarry() {
before();
this.target.HappyMarry();
after();
}
private void after() {
System.out.println("结婚后干的事....");
}
private void before() {
System.out.println("结婚前干的事.....");
}
}
4. Lambda表达式
4.1为什么要使用lambda表达式
- 避免匿名内部类定义过多
- 可以让你的代码看起来很简洁
- 去掉一堆没有意义的代码,只留下核心的逻辑
4.2 函数式接口的定义
任何接口,如果只包含唯一一个抽象方法, 那么它就是一个函数式接口 --Functional Interface;
public interface Runnable{
public abstract void run();
}
对于函数式接口,可以通过lambda表达式来创建该接口的对象:
- lambda 表达式只能有一行代码的情况下才能简化成为一行,如果有多行,那么就用代码块包裹;
- 前提: 接口为函数式接口
- 多个参数也可以去掉参数类型, 要去掉就都去掉, 必须加上括号
4.3 局部内部类,匿名内部类
public class TestLambda {
//3. 静态内部类
static class Like2 implements ILike{
@Override
public void lambda() {
System.out.println("i like lambda2");
}
}
public static void main(String[] args) {
ILike like = new Like();
like.lambda();
ILike like2 = new Like2();
like2.lambda();
//4.局部内部类
class Like3 implements ILike{
@Override
public void lambda() {
System.out.println("i like iambda3");
}
}
ILike like3 = new Like3();
like3.lambda();
//5.匿名内部类,没有类的名称,必须借助几口或者父类
ILike like5 = new ILike() {
@Override
public void lambda() {
System.out.println("i like lambda5");
}
};
like5.lambda();
//6. 用lambda简化
ILike lamLike = ()->{
System.out.println("i like lambda :()->{}");
};
lamLike.lambda();
}
}
//1. 定义一个函数式接口
interface ILike{
void lambda();
}
//2. 实现类
class Like implements ILike{
@Override
public void lambda() {
System.out.println("i like lambda");
}
}
5. 线程状态
5.1 停止线程
- 不推荐使用jdk提供的stop() , destroy() 方法;
- 推荐线程自己停下来;建议使用一个标志位来终止 :
当flag=false,则终止线程运行
public class StopThread implements Runnable{
//1.线程中定义线程体使用的表示
boolean flag = true;
@Override
public void run() {
//2.线程体使用该标识
while (flag){
System.out.println("run thread ");
}
}
//3. 对外提供方法改变标识
public void stop(){
this.flag=false;
}
public static void main(String[] args) {
StopThread st = new StopThread();
new Thread(st).start();
for (int i = 0; i < 1000; i++) {
System.out.println("------"+i);
if(i==988){
st.stop(); //主线程让st线程停止
}
}
}
}
5.2 线程休眠
- sleep(时间) : 当前线程阻塞的毫秒数
- sleep存在Interru’ptedException;
- 时间到达后线程进入就绪状态;
- 可以模拟网络延时,倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
5.3 线程礼让–Yield
- 礼让线程,让当前正在执行的线程暂停,但不阻塞;
- 将线程从运行状态装维就绪状态;
- 让cpu重新调度,礼让不一定成功,看cpu心情;
5.4 Join
- Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
- 可以把它想象成强制插队
Thread th = new Thread();
th.start();
th.join();// 插队执行
5.5 线程状态观测
Thread.State
- NEW : 尚未启动的线程处于此状态;
- RUNNABLE: 在Java虚拟机中执行的线程处于此状态
- BLOCKED : 被阻塞等待监视器锁定的线程出此状态;
- WAITING : 正在等待另一个线程执行特定动作的线程处于此状态;
- TIMED_WAITING: 正在等待另一个线程指定动作达到指定等待时间的线程处于此状态
- TERMINATED : 已退出的线程处于此状态;
5.6 线程优先级
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行.
- 线程的优先级用数字表示,范围从1~10
Thread.MIN_PRIORITY=1;
Thread.MAX_PRIORITY=10;
Thread.NORM_PRIORITY=5; - 使用一下方式改变或获取优先级
getPriority() setPriority(int xx)
优先级的设置在start()调度前
5.7 守护线程 --daemon
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不必等待守护线程执行完毕
Thread.setDaemon(true);
虚拟机在用户线程跑完后程序停止, 不会考虑守护线程
6 线程同步
多个线程操作同一个资源
- 并发: 同一个对象被多个线程同时操作
还可以使用安全类型
public static void main(String[] args) {
// ArrayList<String> list = new ArrayList<>();
// for (int i = 0; i < 1000; i++) {
// new Thread(()->{
// list.add(Thread.currentThread().getName());
// }).start();
// }
// try {
// Thread.sleep(3000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(list.size());
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
上面的ArrayList 未加锁,不安全,线面的CopyOnWriteArrayList类型也没有加锁,但是是安全的类型
7. 死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形. 某一个同步块同时拥有"两个以上对象的锁"时,就可能会发生死锁的问题;
多个线程互相抱着对方需要的资源,然后形成僵持
避免死锁的方法:
产生死锁的四个必要条件
- 互斥条件: 一个资源每次只能被一个进程使用
- 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件: 进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
以上四个死锁的必要条件,只需破坏其中一个或多个就可避免死锁的发生
8. Lock(锁)
通过显示定义同步锁对象来实现同步. 同步锁使用Lock对象充当 (jdk5.0)
Lock接口是控制多个线程对共享资源进行访问的工具.
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象.
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中, 比较常用的是ReentrantLock
,可以显示加锁、释放锁。
未加锁的代码
public class LockTest {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
//多线程对象
class TestLock2 implements Runnable{
int ticketNums = 10;
@Override
public void run() {
while (true){
if(ticketNums > 0){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}
}
}
加锁后
//这个多线程加上lock锁后为什么只有线程a在执行???
public class LockTest {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2,"a").start();
new Thread(testLock2,"b").start();
new Thread(testLock2,"c").start();
}
}
//多线程对象
class TestLock2 implements Runnable{
int ticketNums = 10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();
if(ticketNums > 0){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
System.out.println(Thread.currentThread().getName());
}else {
break;
}
}finally {
lock.unlock();
}
}
}
}
8.1 synchronized与Lock的对比
- Lock是显示锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好. 并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序: Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
9.线程协作与通信
生产者消费者场景理解:
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止;
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
- 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
- 在生产者消费者问题中,仅有synchronized是不够的
-
- synchronized可阻止并发更新同一个共享资源,实现了同步
-
- synchronized 不能用来实现不同线程之间的消息传递(通信)
java提供了几个方法解决线程之间的通信问题
wait() : 表示线程一直等待,知道其他线程通知,与sleep不同,会释放锁
9.1利用缓冲区解决(管程法)
9.2 信号灯法(标志位解决)
10 . 线程池
public class TestPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
11 线程启动方式的总结
public class ThreadStudy {
public static void main(String[] args) {
new MyThread1().start();
new Thread(new MyThread1()).start();
new Thread(new MyThread2()).start();
//启动实现Callable的方式
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread3());
new Thread(futureTask).start();
try {
Integer integer = futureTask.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread1 extends Thread{
@Override
public void run() {
System.out.println("MyThread1");
}
}
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("MyThread2");
}
}
class MyThread3 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("MyThread3");
return 100;
}
}