第二章、线程
什么是线程
线程是指进程中的一个流程、一个进程可以运行多个线程。每一个任务称为一个线程,同时运行一个以上线程的程序称为多线程程序。一般常见的java应用程序都是单线程的。比如,运行一个hello world程序,就启动了一个JVM进程,JVM找到程序的入口点main(),然后运行main()方法,这样就产生了一个线程,这个线程称为主线程,当main方法结束后,主线程运行完成。JVM进程也随即退出。
注意:操作系统每启动一个进程,就会分配一个独立的存储空间,而线程是不存在独立的地址空间的,但是可以共享同一进程的公共资源,同时拥有运行时的一些资源。同一进程中的线程可以并发执行,每一个线程完成一个功能,这种机制称为多线程。
实现线程的方式
1.继承Thread类
public class ThreadDemo extends Thread{
@Override
public void run() {
System.out.println("hello world");
//打印出线程名
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
}
}
2.实现Runnable接口
public class RunnableDemo implements Runnable{
@Override
public void run() {
System.out.println("hello world");
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
分析:
public
class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
通过分析Thread的源代码发现Thread类实现了Runnable接口,具体实现了Runnable中的run方法。
通过继承Thread类可以将run方法覆盖。然后start执行的就是继承Thread类中的run方法。
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
这里挑选了3中Thread的构造方法 实际上有八种 自己去看源码即可
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
public static native Thread currentThread();
private static volatile SecurityManager security = null;
private ThreadGroup group;
public static SecurityManager getSecurityManager() {
return security;
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//如果线程名为空则抛出异常
if (name == null) {
throw new NullPointerException("name cannot be null");
}
//给定线程名
this.name = name;
//Thread.currentThread() 返回的是 一个实例。 只不过呢, 这个实例确实比较特殊。 这个实例是当前Thread 的引用。Thread.currentThread() 当然 不等于 Thread。
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
通过分析第一个构造方法:因为没有传入其他参数 默认名称为"Thread-" + nextThreadNum() 即第一次创建一个线程名为Thread-0
第二个方法传入了Runnable接口,在集合那一章节讲过Runnable接口属于函数式接口,则可以使用Lamda表达式来创建一个线程
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
new Thread(()->{
System.out.println("hello");
}).start();
第三种构造方法只是多加了一个线程名 这里就不做分析了
线程的生命周期
这里通过图解的方式给出:
出生:例如Thread t = new Thread();
就绪:t.start()
注意:start后不是立即执行,需要得到系统资源后才进入到运行状态
一旦线程进入可执行状态,它就会在就绪与运行状态下切换,同时可能进入等待、休眠、阻塞或死亡状态。当处于运行状态下的线程调用Thread类中的wait方法,则该线程进入等待状态、必须调用notify才能唤醒,而notify()方法则是将所有处于等待状态下的线程唤醒。
操作线程的方法
线程的休眠
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
上面的代码表示线程1秒内不能进入就绪状态。由于sleep方法可能会抛出异常,所以放在try catch 语句块中。
线程的加入
Thread threadB = new Thread(()->{System.out.println(Thread.currentThread().getName());},"B");
Thread threadA = new Thread(()->{try {
threadB.join();
}catch (Exception e){
e.printStackTrace();
}System.out.println(Thread.currentThread().getName());},"A");
threadA.start();
threadB.start();
//执行结果为 B A
线程的中断
通过调用interrupted方法,但是会抛出中断异常,自己需要处理异常
通过在run方法类使用无限循环的模式,达到某一条件后跳出循环即可。
线程的礼让
yield()方法。
这只是一种暗示,没有任何一种机制保证当前线程会将资源礼让。
线程的优先级
如果有多个线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态。
注意:但并不意味着低优先级的线程得不到运行,只是运行的几率小,如垃圾回收线程的优先级就较低。
线程的优先级可以使用setPriority()方法调整。
Thread类中包含的成员变量代表了线程的某些优先级。
Thread.MIN_PRIORITY;1
Thread.MAX_PRIORITY;10
Thread.NORM_PRIORITY;5
注意:默认情况下其优先级都是5且每个线程的优先级都在1-10之间
线程安全
同步机制
同步块 synchronized修饰的代码块
同步方法 synchronized修饰的方法
卖票示例:
public class SafeTickets {
private int count = 50;
//同步方法
public synchronized void safeTicket() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + count + "张票");
count--;
} else {
return;
}
}
//同步块
public void safeTickets() {
synchronized (this){
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + count + "张票");
count--;
}
}
}
public static void main(String[] args) {
SafeTickets safeTickets = new SafeTickets();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
//safeTickets.safeTicket();
safeTickets.safeTickets();
}, String.valueOf(i)).start();
}
}
}
这里只是简单提一下 具体线程安全问题会在并发编程介绍中去讲解
并发编程
目的:并发编程是为了让程序运行的更快,而不是为了启动多的线程去并发完成任务。
并发编程的底层实现原理:
首先要明白的是Java的执行过程:java文件被编译成字节码文件,字节码文件被类加载器加载到jvm中,jvm执行字节码,最终转化为机器码在CPU中运行。java中使用的并发机制依赖于JVM的实现和CPU的指令。
synchronized的应用
先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
·对于普通同步方法,锁是当前实例对象。
·对于静态同步方法,锁是当前类的Class对象。
·对于同步方法块,锁是Synchonized括号里配置的对象。
synchronized用的锁是存储到对象头当中的,如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。
对象头内容
volatile的应用
可见性:
volatile可以理解为轻量级的synchronized,它保证了线程执行过程中的可见性,也就是说一个线程修改一个变量后,另外一个线程能够读到修改后的值,它的执行成本更低,不会造成上下文切换和调度。
上下文切换:CPU分配时间片有限,多个线程切换执行。
在Java中,如果一个字段被volatile修饰,Java线程内存模型确保所有线程看到这个变量的值是一致的。
请看这个例子:当flag被某一线程修改为true时 会通知其他线程flag的值被修改
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class KjDemo {
private int a=0;
//注意:先测试不加volatile会发生什么情况,然后加上volatile之后是否解决
private volatile boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void reader(){
if(flag){
System.out.println(Thread.currentThread().getName()+"a");
flag = false;
}
}
public static void main(String[] args) {
KjDemo kjDemo = new KjDemo();
new Thread(()->{
while (!kjDemo.flag){
}
System.out.println("结束了");
},"A").start();
//休眠3秒 更直观的看运行结果
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(()->{
kjDemo.writer();
},"B").start();
}
}
原子性:
对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
重排序分为2类:编译器重排序和处理器重排序 其中下面2,3两点统一为处理器重排序
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句
的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level
Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应
机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上
去可能是在乱序执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WWSrF2ur-1606707719004)(C:\Users\pzh\AppData\Roaming\Typora\typora-user-images\image-20201124111910024.png)]
锁的内存语义(这一节来源于java并发编程艺术)
public class lockMemory {
int a = 0;
public synchronized void writer() { // 1
a++; // 2
} // 3
public synchronized void reader() { // 4
int i = a; // 5
} // 6
}
//线程B获取的是同一把锁
假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,这个
过程包含的happens-before关系可以分为3类。
1)根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happensbefore 6。
2)根据监视器锁规则,3 happens-before 4。
3)根据happens-before的传递性,2 happens-before 5。
happens-before介绍:
从JDK 5开始,Java使用新的JSR-133内存模型(除非特别说明,本文针对的都是JSR-133内
存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一
个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关
系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
与程序员密切相关的happens-before规则如下。
·程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
·监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
·volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的
读。
·传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
java保证原子性
package classs.current;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileDemo {
private int i = 0;
private AtomicInteger atomicInteger = new AtomicInteger(0);
public void add(){
i++;
}
public void atomicAdd(){
atomicInteger.getAndIncrement();
}
public static void main(String[] args) {
VolatileDemo volatileDemo = new VolatileDemo();
for (int i = 0; i < 100; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
volatileDemo.add();
volatileDemo.atomicAdd();
}
},String.valueOf(i)).start();
}
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(volatileDemo.i);
System.out.println(volatileDemo.atomicInteger.get());
}
}
ThreadLocal
定义:ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
示例:计算begin()到end()执行的时间:
public class ThreadLocalDemo {
// 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次
private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
protected Long initialValue() {
return System.currentTimeMillis();
}
};
public static final void begin() {
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
public static final long end() {
return System.currentTimeMillis() - TIME_THREADLOCAL.get();
}
public static void main(String[] args) throws Exception {
ThreadLocalDemo.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println("Cost: " + ThreadLocalDemo.end() + " mills");
}
}