基本概念
什么叫进程?什么叫进程?
进程: 假设你的硬盘上有一个“英雄联盟”的程序,这就是一个程序,是一种静态的概念,当你打开它,弹出一个界面输入账号密码,这个时候就叫做一个进程,进程相对程序来说是一种动态的概念。
线程: 作为一个进程里面最小的执行单元就叫做一个线程,简单的说就是一个程序里不同的执行路径叫做线程。
package com.learn.thread.first;
import java.util.concurrent.TimeUnit;
/**
* 认识线程
*/
public class TestThread {
private static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
TimeUnit.MICROSECONDS.sleep(1);
}catch (InterruptedException ex) {
}
System.out.println("T1");
}
}
}
public static void main(String[] args) {
// 开启一个线程去执行
new T().start();
for (int i = 0; i < 10; i++) {
try {
TimeUnit.MICROSECONDS.sleep(1);
}catch (InterruptedException ex) {
}
System.out.println("main");
}
}
}
观察上面程序的结果,你会发现T1和main交替输出,这就是程序中两条不同的执行路径在交叉执行。
创建线程的几种方式
package com.learn.thread.first;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
/**
* 创建线程的三种方式
*/
public class CreateThread {
private static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello MyThread");
}
}
private static class MyRunnable implements Runnable {
public void run() {
System.out.println("Hello myRunnable");
}
}
private static class MyCallable implements Callable<String> {
public String call() throws Exception {
System.out.println("Hello MyCallable");
return "success";
}
}
public static void main(String[] args) {
// 执行线程的五种方法
new MyThread().start();
new Thread(new MyRunnable()).start();
new Thread(() -> System.out.println("hello lamdba")).start();
new Thread(new FutureTask<String>(new MyCallable())).start();
ExecutorService service = Executors.newFixedThreadPool(4);
service.execute(() -> System.out.println("hello ThreadPool"));
service.shutdown();
}
}
分享一道面试题
启动线程的三种方式
- new Thread().start()
- new Thread(new Runnable()).start()
- 线程池fututreTask 或者 callable
认识几个线程的方法
sleep
sleep的意思就是说睡眠,当前线程暂停一段时候让给别的线程去执行。Sleep是怎么复活的由你的睡眠时间而定,等睡眠到了规定时间就自动恢复。
public static void testSleep() {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("A" + i);
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Yield
Yield就是当前线程正在执行的时候停止下来进入等待队列,回到等待队里里在系统的调度算法里还是有可能可能吧你刚回去的这个线程拿回来继续执行,当然,更大的可能性是把原来等待的线程拿出一个来执行,所以yield的意思就是我让出一下CPU,不管别的线程是否能够抢到。
public static void testYield() {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("A" + i);
if (i % 10 == 0) {
Thread.yield();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("B" + i);
if (i % 10 == 0) {
Thread.yield();
}
}
}).start();
}
Join
join的意思就是在自己当前线程加入你调用join的线程,本线程等待,等调用的线程执行完了,自己再去执行。比如 t1和t2两个线程,t1在某个点上调用了t2.join,它就会跑到t2去执行,t1等待t2运行完毕继续t1运行,但是如果是自己的join则没有任务意义。
public static void testJoin() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("A" + i);
}
});
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
System.out.println("B" + i);
}
});
t1.start();
t2.start();
}
线程状态
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
当我们new一个线程时,还没有调用start,该线程就处于**(1)NEW新建状态。
线程对象调用start方法时候,它就会被线程调度器来执行,也就是交给操作系统来执行了,那么操作系统来执行的时候,这整个的状态就(2)Runnable运行状态**,Runnable内部有两个状态(Ready/Running),Ready状态就是说扔到CPU的等待队列排队等待CPU运行,等CPU真正执行了才叫Running状态。注意这里调用yield时候会从Running状态跑到Ready状态
如果你的线程顺利执行完了就会进去**(3)Teminated结束状态**,注意这里是终态,不可以从new状态再调用start,这是不行的。
在Runnable这个状态还有其他一些状态的变迁**(4)TimedWaiting等待**,(5)Waiting等待,(6)Blocked阻塞,在同步代码块的情况下没得到锁就进入阻塞状态,获得锁就是就绪状态。在运行时候如果调用了o.wait、t.join、LockSupport.park就进入Waiting状态,需要调用o.noifty、o.noiftyAll、LockSupport.unpark就回到Runnning状态。TimedWaiting按时间等待等时间结束了,自己就回去了。
下面我们验证一下
package com.learn.thread.first;
public class ThreadStatus {
static class Mythrad extends Thread {
@Override
public void run() {
System.out.println(this.getState());
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
}catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(i);
}
}
}
public static void main(String[] args) {
Thread t = new Mythrad();
System.out.println(t.getState());
t.start();
System.out.println(t.getState());
try {
t.join();
}catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(t.getState());
}
}
面试题1 哪些是JVM管理的?哪些是操作系统管理的?
上面的这些状态全是由JVM管理的,因为JVM管理的时候也要通过操作系统,所以JVM和操作系统是分不开的。
面试题2 线程什么状态会被挂起,挂起时候也是一个状态?
Running的时候,一个cpu上会跑多个线程,cpu会隔一段时候去执行这个线程一下,隔一段时间去执行另外一个线程一下,这个是CPU内部的一个调度。把Running状态扔回到Ready状态就叫挂起。
Synchronized
下面我们来讲synchronized关键字,先想一下为什么要上锁,如果访问一段代码或者临界资源的时候是需要一把锁的概念在这。
比如:我们读一个数字做递增,两个线程对它一块进行递增,如果两个线程共同访问的时候,第一个线程读它是0,然后把自己加1,但是如果线程没有往内存写回去的时候,第二个线程过来读,读到的是0,结果两个线程的递增,本来增加了两次,但是结果还是1。这里就用到了锁的概念,线程对这个数字的访问时独占的,不允许别的线程线程来进行计算。
先来看看线程锁程序
package com.learn.thread.first;
public class SynchronizedTest {
private static int count = 1;
private static Object o = new Object();
public static void m() {
synchronized (o) {
if (count > 0) {
count --;
}
System.out.println(Thread.currentThread().getName() + "count = " + count);
}
}
public static void main(String[] args) {
for (int i = 0; i< 100; i ++) {
new Thread(() -> {
SynchronizedTest.m();
}).start();
}
}
}
/**
* 这里的效果和m3的效果是一样的
*/
public void m2() {
synchronized (this) {
if (count > 0) {
count --;
}
System.out.println(Thread.currentThread().getName() + "count = " + count);
}
}
/**
* 这里的m2是一样的
*/
public synchronized void m3() {
if (count > 0) {
count -- ;
}
System.out.println(Thread.currentThread().getName() + "count = " + count);
}
public void m4() {
synchronized (SynchronizedTest.class) {
if (count > 0) {
count -- ;
}
System.out.println(Thread.currentThread().getName() + "count = " + count);
}
}
public static void m5() {
synchronized (SynchronizedTest.class) {
if (count > 0) {
count -- ;
}
System.out.println(Thread.currentThread().getName() + "count = " + count);
}
}
// 这种方式不可行
// public static void mmm() {
// synchronized (this) {
// count --;
// System.out.println(Thread.currentThread().getName() + " count = " + count);
// }
// }
面试题1: T.class是单例的吗?
一个Class load到内存是不是单例的,一般情况下是,如果是在同一个ClassLoder空间,那它一定是,不是同一个类加载器就不是了,不同的类加载器互相不能访问,所以说能访问它,就一定是单例!
我们再来看看一个很有可能读不到别的线程修改过内容的程序
package com.learn.thread.first;
public class Account {
String name;
double blance;
public synchronized void setBlacne(String name, double blance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
this.blance = blance;
}
/**
*
* 不加锁,产生脏读现象
* @param name
* @return
*/
public double getBlance(String name) {
return this.blance;
}
/**
*
* 不加锁,产生脏读现象
* @param name
* @return
*/
public synchronized double getBlanceSync(String name) {
return this.blance;
}
public static void main(String[] args) {
Account account = new Account();
new Thread(() -> {
account.setBlacne("zlx",100.0);
}).start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(account.getBlance("zlx"));
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(account.getBlanceSync("zlx"));
}
}
如图所述,当用getBlance获取的时候,产生了脏读现象,是因为setBlance睡了1000加锁,getBlance是没加锁的,所以异步的时候,获取的blance是0,但是getBlanceSync也是加锁的,锁的也是当前对象,所以不会产生脏读。
可重入
synchronized是可重入的,有一个方法m1是synchronized,另外一个方法m2也是synchronized,我们在m2开始的时候获取到了这把锁,然后m1里面调用m2,如果说这个时候不允许任务线程再来获取锁,就会造成死锁,锁以m2也需要申请锁,它发现同一个线程已经申请到了锁,也就让m2继续运行了,这就叫可重入锁。
嵌套锁
package com.learn.thread.first;
public class T {
private static int count = 10;
/**
* 一个同步方法调用另外一个同步方法,一个线程已经拥有了某个对象的锁,该线程再次申请的时候仍然会得到该对象的锁
* 这就是可重入
*
* 这里我们模拟减两次
*/
public synchronized static void m1() {
count --;
System.out.println("m1 " + Thread.currentThread().getName() + " count = " + count);
m2();
}
public synchronized static void m2() {
count --;
System.out.println("m2 " + Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> T.m1()).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里我们模拟一个父类子类的概念,父类的方法synchronized,子类调用super.m的时候必须得可重入,否则就会出现问题,调用父类是同一把锁,所谓的可重入锁就是你拿到这个锁以后不停的加锁加锁,加好几道,但是加的锁都是同一个对象,去一道就减1。
父子锁
static class TT extends T {
@Override
public synchronized void m1() {
count --;
System.out.println("m1 son " + Thread.currentThread().getName() + " count = " + count);
super.m1();
}
}
异常锁
我们来看看异常锁的情况,加一个while(true)循环,当等于15的时候产生异常,我们看看如果这个时候产生异常,会出现什么情况?
/**
* 程序在执行过程中,如果出现异常,默认情况下锁会被释放
* 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况下
* 比如 一个webapp处理过程中,多个请求实例共同访问同一个资源,这时如果异常处理不合适
* 在第一个县城中抛出异常,其他线程就会进入同步代码块,有可能会访问到异常产生时的数据
*
*/
public synchronized void m3() {
System.out.println(Thread.currentThread().getName() + "start");
while (true) {
count ++;
System.out.println(Thread.currentThread().getName() + "count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
}catch (Exception ex) {
ex.printStackTrace();
}
if (count == 15) {
// 这里产生异常,锁被释放,如果想不被释放,可以在这里进行catch,然后让循环继续
int i = 1/0;
System.out.println(i);
}
}
}
线程2 开始之后居然打印了线程1的异常信息
synchronized的底层实现
早期时候的java的synchronized底层实现时重量级的,需要找操作系统去申请锁,就会造成效率非常低下,java后来进行了锁升级
从原来都去找操作系统,现在是线程进来之后先去访问锁对象,在这个锁对象的对象头记录一下这个线程的ID,如果这个锁对象存在多个线程ID,那么就升级为自旋锁,概念就是不会跑到cpu的就绪队列取等待执行,而是用一个while循环,相当于一个插队的概念,如果循环十几次都没有拿到锁,就升级为重量级锁,去操作系统请求申请这个锁对象。
执行时间短(加锁代码),线程数少,用自旋锁
执行时间长,线程数多,去操作系统申请锁
至于锁升级的详细过程,以后更新