学习笔记——线程
一、学习重点
二、学习内容
join方法
public static void main(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) { try { Thread.sleep(100); System.out.println("这是线程1-->"); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10; i++) { try { Thread.sleep(100); System.out.println("这是线程2-->"); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); t2.start(); try { //插队 t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-------------------------------------"); //分割线出现的位置,join方法的本意阻塞线程 }
车票
public class Ticket implements Runnable{ private static final Object lock = new Object(); private static Integer count = 100; String name; public Ticket(String name) { this.name = name; } @Override public void run() { while(Ticket.count > 0){ try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (Ticket.lock){ System.out.println(name + "出票一张,还剩:" + Ticket.count-- + "张!"); } } } public static void main(String[] args) { Thread one = new Thread(new Ticket("一号窗口")); Thread two = new Thread(new Ticket("二号窗口")); one.start(); two.start(); } }
三、笔记内容
创建线程
在Java中,创建线程有3种方式
(1)继承Thread,并且重写run方法
Thread中的run方法不是抽象方法。Thread类也不是抽象类。
MyThread当继承了Thread类之后,他就是一个独立的线程。
要让线程启动。调用线程的start方法
class MyThread extends Thread { @Override public void run() { System.out.println(2); } } public class Ch01 { public static void main(String[] args) { System.out.println(1); MyThread myThread = new MyThread(); //当调用start方法启动一个线程时,会执行重写的run方法的代码 //调用的是start,执行的是run,为什么不直接调run // 调run不是启动线程,只是普通的对象调方法 myThread.start(); //线程的优先级,概率问题!做不到百分百 System.out.println(3); System.out.println(4); } }
(2)实现Runnable(函数式)接口
class MyThread2 implements Runnable{ @Override public void run() { System.out.println(2); } } public class Ch02 { public static void main(String[] args) { System.out.println(1); MyThread2 myThread2 = new MyThread2(); //如果想要线程启动必须调用Thread类中的start方法 //问题:实现了Runable接口,找不到start方法了? Thread t = new Thread(myThread2); t.start(); System.out.println(3); System.out.println(4); } }
使用箭头函数(Lambda表达式)前提:函数式接口
public static void main(String[] args) { System.out.println(1); //箭头函数接口,抽象类,重写方法 new Thread(() -> System.out.println(2)) .start(); try { Thread.sleep(1000); } catch (InterruptedException e) {//线程终止异常 e.printStackTrace(); } System.out.println(3); System.out.println(4); }
(3)实现Callable接口
class MyThread3 implements Callable<String>{ @Override public String call() throws Exception { System.out.println(2); return "call方法的返回值"; } } public class Ch04 { public static void main(String[] args) { System.out.println(1); //Callable --> FutureTask --> RunableFuture --> Runable -->Thread FutureTask<String> futureTask = new FutureTask<>(new MyThread3()); new Thread(futureTask).start(); System.out.println(3); System.out.println(4); } }
守护线程
Java中提供两种类型的线程:
1、用户线程
2、守护程序线程
守护线程为用户线程提供服务,仅在用户线程运行时才需要
守护线程对于后台支持的任务非常有用。
垃圾回收,大多数JVM线程都是守护线程。
QQ,主程序就是用户线程
创建守护线程
任何线程继承创建它的线程守护进程状态。由于主线程是用户线程
因此在main方法类
public class Ch05 extends Thread{ @Override public void run() { super.run(); } public static void main(String[] args) { Ch05 ch05 = new Ch05(); ch05.setDaemon(true); ch05.start(); } }
线程的生命周期******
从摇篮到坟墓
NEW:这个状态主要是线程未被start()调用执行
RUNNABLE:线程正在JVM中被执行,等待来自操作系统的调用
BLOCKED:阻塞。因为某些原因不能立即执行需要挂起等待
WAITING:无限期等待。Object类。如果没有唤醒,则一直等待。
TIMED_WAITING:有限期等待,线程等待一个指定的时间
TERMINATED:终止线程的状态,线程已执行完毕。
等待和阻塞两个概念相似,阻塞因为外部原因,需要等待
而等待一般是主动调用方法,发起主动的等待。等待可以传入参数确定等待的时间
public static void sleep(int i){ try { Thread.sleep(i); System.out.println("哈哈哈..."); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { sleep(1000);//1000ms }
CPU多核缓存结构
物理内存:硬盘内存。(固态硬盘,尽量不要选择混合硬盘)
CPU缓存为了提高程序运行的性能,现在CPU在很多方面会对程序进行优化
CPU处理速度最快,内存次之,硬盘速度最低
在CPU处理内存数据时,如果运行速度太慢,就会拖累CPU的速度
为了解决这样的问题,CPU设计了多级缓存策略
CPU分为三级缓存:每个CPU都有L1,L2缓存,但是L3缓存是多核公用的
CPU查找数据时:CPU --> L1 --> L2 --> L3 -> 内存 --> 硬盘
从CPU到内存,60~80ns
从CPU到L3,15ns
从CPU到L2,3ns
从CPU到L1,1ns
寄存器,0.3ns
进一步优化,CPU每次读取一个数据,读取时与它相邻的64个字节的数据.
【缓存行】
英特尔提出了一个协议MESI协议
1、修改态,此缓存被动过,内容与主内存中不同,为此缓存专有
2、专有态,此缓存与主内存一致,但是其它CPU没有
3、共享态,此缓存与主内存一致,其它的缓存也有
4、无效态,此缓存无效,需要从主内存中重新读取
【指令重排】
Java内存模型-JVM
尽量做到硬件和操作系统之间达到一致的访问效果。
{ int a = 1;//1 int b = 2;//2 int c = a + b;//3 /* 指令3不能被排到1和2前面 但是1和2之间没有依赖关系,编辑器就可以重拍1和2。 不会对程序的执行顺序产生干扰。 */ } public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5}; for (int i = 0; i < nums.length; i++) { System.out.println(nums[i]); } }
测试
private static int x = 0,y = 0; private static int a = 0,b = 0; private static int count = 0; private static volatile int NUM = 1; public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); for (;;){ Thread thread1 = new Thread(() ->{ a = 1; x = b; }); Thread thread2 = new Thread(() ->{ b = 1; y = a; }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("一共执行了"+count++ + "次"); if (x == 0 && y ==0){ long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start) + "毫秒,(" + x + y + ")"); break; } a=0;b=0;x=0;y=0; } /* 我们发现测试结果中大部分感觉是正确的,(0,1)或(1,0),一个是线程1先执行,一个是线程2先执行。 按道理来说,绝对不会出现(0,0),如果出现(0,0)代表存在指令重排,乱序执行。 使用volatile关键字来保证一个变量在一次读写操作时,避免指令重排。 我们在读写操作之前加入一条指令,当CPU碰到这条指令后必须等到前面的执行执行完成才能继续执行下一条指令。 【内存屏障】 */
可见性
thread线程一直在高速读取缓存中的isOver,不能感知主线程已经把isOver改变
这就是线程的可见性问题
解决方法:
volatile能够强制改变变量的读写直接在内存中操作
private static volatile boolean isOver = false; private static int nuumber = 0; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { while (!isOver){ } System.out.println(nuumber); } }); thread.start(); Thread.sleep(1000); nuumber = 50; //已经改了,为什么循环未退出 //可见性,守护线程与主线程隔离 isOver = true; }
线程争抢
解决线程争抢的问题最好的办法就是【加锁】
synchronized同步锁,线程同步
当一个方法加上了synchronized修饰,这个方法就是同步方法
private static int count = 0; public synchronized static void add(){ count ++; } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { add(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { add(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("最后的结果是" + count); }
线程安全的实现方法
(1)数据不可变。
一切不可变的对象一定是线程安全的
对象的方法的实现方法的调用者,不需要再进行任何的线程安全的保障措施
比如final关键字修饰的基本数据类型,字符串
只要一个不可变的对象被正确的创建出来,那外部的可见状态永远都不会改变
(2)互斥同步。加锁。【悲观锁】
(3)非阻塞同步。【无锁编程】,自旋,我们会用cas来实现这种非阻塞同步
(4)无同步方案。多个线程需要共享数据,但是这些数据又可以在单独的线程计算,得出结果
我们可以把共享数据的可见范围限制在一个线程之内,这样就无需同步。把共享的数据拿过来
我用我的,你用你的,从而保证线程安全。ThreadLocal
private final static ThreadLocal<Integer> number = new ThreadLocal<>(); private static int count = 0; public static void main(String[] args) { Thread thread1 = new Thread(new Runnable() { @Override public void run() { // t1内部自己用一个count number.set(count); for (int i = 0; i < 10; i++) { number.set(i++); System.out.println("t1------"+number.get()); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { // t2内部自己用一个count number.set(count); for (int i = 0; i < 10; i++) { number.set(i++); System.out.println("t2------"+number.get()); } } }); thread1.start(); thread2.start(); }