创建线程(3种)
1.继承Thread类,并重写run()方法
Thread类中的run方法不是抽象方法,Thread类也不是抽象类 java是单继承,所以只能继承Thread一个类,所以Thread类一般用来启动线程的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 myThread.start(); // 普通的对象调方法 // myThread.run(); // 线程的优先级,概率问题!做不到百分百 // 90会先跑主方法 10先跑mythread System.out.println(3); System.out.println(4); } }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 myThread.start(); // 普通的对象调方法 // myThread.run(); // 线程的优先级,概率问题!做不到百分百 // 90会先跑主方法 10先跑mythread System.out.println(3); System.out.println(4); } }
2.实现Runnable接口,重写run()方法,并用Thread类来包装
接口是多实现,没有单继承的限制
实现Runnable接口是把run()方法写到接口中然后再用Thread类中的start()方法来启动线程
箭头函数(lambda表达式)public class Ch03 { public static void main(String[] args) { /*箭头函数接口:抽象类重写方法*/ 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,重写call()方法,包装成FutureTask,然后再包装成Thread
有返回值,借助FutureTask类来判断线程是否执行完毕或取消
class MyThread3 implements Callable<String>{ @Override public String call() throws Exception { System.out.println(2); return "call方法的返回值"; } } public class Ch04 { /*实现Callable接口*/ public static void main(String[] args) { System.out.println(1); FutureTask<String> futureTask = new FutureTask<>(new MyThread3()); new Thread(futureTask).start(); System.out.println(3); System.out.println(4); } }
RunnableFuture
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
FutureTask
public class FutureTask<V> implements RunnableFuture<V> { // 构造函数 public FutureTask(Callable<V> callable); // 取消线程 public boolean cancel(boolean mayInterruptIfRunning); // 判断线程 public boolean isDone(); // 获取线程执行结果 public V get() throws InterruptedException, ExecutionException; }
Callable 也是一种函数式接口
@FunctionalInterface public interface Callable<V> { V call() throws Exception; }
三种线程的比较
Thread: 继承方式为单继承, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行
当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程
Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现
守护线程 java中提供两种线程 1.用户线程 2.守护程序线程 守护线程为用户线程提供服务,仅在用户线程运行时才需要 守护线程自己启动不了 守护线程对于后台支持任务非常有用 垃圾回收 大多数jvm线程都是守护线程 创建守护线程 任何线程继承创建他的线程守护线程状态,由于主线程是用户线程 在main方法内启动的任何线程默认都是守护线程
线程的生命周期
NEW:这个状态主要是线程未被start()调用执行 RUNNABLE;线程正在jvm中被执行,等待来自操作系统的调度 BLOCKED:阻塞,因为某些原因不能立即执行需要挂起等待 WAITING:无限期等待 object类 如果没有唤醒,就一直等、 TIME_WAITING:有限期等待,线程等待一个指定时间 TERMINATED:终止线程的状态,线程已经执行完毕等待和阻塞概念相像 阻塞因为外部原因需要等待 等待是主动等待发起主动的等待 等待还可以传入参数确定等待 等待一般是主动调用方法
CPU多核缓存结构 物理内存:硬盘内存。(固态硬盘,尽量不要选择混合硬盘) CPU缓存为了提高程序运行的性能,现在CPU在很多方面对程序进行优化。 CPU处理速度最快,内存次之,硬盘速度最低。 在CPU处理内存数据时,如果内存运行速度太慢,就会拖累CPU的速度 为了解决这样的问题,CPU设计了多级缓存策略。 CPU分为三级缓存:每个CPU都有L1,L2缓存,但是L3缓存是多核公用的。 CPU查找数据时,CPU->l1->l2->l3->内存->硬盘 从CPU到内存,60-80纳秒 从CPU到L3,15纳秒 从CPU到L1,1纳秒 寄存器,0.3纳秒 进一步优化,CPU每次读取一个数据,读取的时与它相邻的64个字节的数据。 【缓存行】。 英特尔提出了一个协议MESI协议 1、修改态,此缓存被动过,内容与主内存中不同,为此缓存专有 2、专有态,此缓存与主内存一致,但是其他CPU中没有 3、共享态,此缓存与主内存一致,其他的缓存也有 4、无效态,此缓存无效,需要从主内存中重新读取 【指令重排】 四条指令,四个人在四张纸上写下【恭喜发财】。 java内存模型-JMM 尽量做到硬件和操作系统之间达到一致的访问效果。 指令重排练习public class Ch02 { private static int x = 0,y = 0; private static int a = 0,b = 0; private static int count = 0; private volatile static int NUM = 1; public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); for (;;) { Thread t1 = new Thread(() -> { a = 1; x = b; }); Thread t2 = new Thread(() -> { b = 1; y = a; }); t1.start(); t2.start(); t1.join(); t2.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碰到这条指令后必须等到前面的执行执行完成才能继续执行下一条指令。 【内存屏障】。 */ }
可见性
hread线程一直在高速读取缓存中的isOver,不能感知主线程已经把isOVer改成了true 这就是线程的可见性的问题。volatile能够强制改变变量的读写直接在内存中操作。
线程争抢
解决线程争抢的问题最好的办法就是【加锁】synchronized同步锁,线程同步 当一个方法加上了synchronized修饰,这个方法就叫做同步方法。
线程安全的实现方法
(1)数据不可变。 一切不可变的对象一定是线程安全的。 对象的方法的实现方法的调用者,不需要再进行任何的线程安全的保障措施。 比如final关键字修饰的基本数据类型,字符串。 只要一个不可变的对象被正确的创建出来,那外部的可见状态永远都不会改变。 (2)互斥同步。加锁。【悲观锁】 (3)非阻塞同步。【无锁编程】,自旋。我们会用cas来实现这种非阻塞同步。 (4)无同步方案。多个线程需要共享数据,但是这些数据又可以在单独的线程中计算,得出结果 我们可以把共享数据的可见范围限制在一个线程之内,这样就无需同步。把共享的数据拿过来, 我用我的,你用你的,从而保证线程安全。ThreadLocal
买票练习
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();
}
}