一、什么是线程?
一个程序里不同的执行路径就是一个线程,是进程里面最小的执行单元。
- 例子:
public class test {
private static class T1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T1");
}
}
}
public static void main(String[] args) {
//new T1().run();
new T1().start();
for (int i = 0; i < 10; i++) {
try {
TimeUnit.MICROSECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main");
}
}
}
- new T1().run()时输出:
T1
T1
T1
T1
T1
T1
T1
T1
T1
T1
main
main
main
main
main
main
main
main
main
main
- new T1().start()时输出:
main
T1
main
T1
main
T1
main
T1
main
T1
T1
main
main
T1
T1
main
T1
main
T1
main
二、run() 和start()的区别?
调run()方法时候只有一条执行路径,调start()时候是run方法和mian方法同时进行是两条执行路径。
三、什么是进程?
做一个简单的解释,你的硬盘上有一个简单的程序,这个程序叫QQ.exe,这是一个程序,这个程序是一个静态的概念,它被扔在硬盘上也没人理他,但是当你双击它,弹出一个界面输入账号密码登录进去了,OK,这个时候叫做一个进程。进程相对于程序来说它是一个动态的概念
四、线程的创建方式有几种?
- 第一种,通过继承Thread类创建线程类
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello MyThread!");
}
}
- 第二种,通过实现Runnable接口创建线程类
static class MyRun implements Runnable {
@Override
public void run() {
System.out.println("Hello MyRun!");
}
}
- 第三种,通过Callable和Future接口创建线程
static class MyCall implements Callable<String> {
@Override
public String call() {
System.out.println("Hello MyCall");
return "success";
}
}
五、启动线程的5种方式
public static void main(String[] args) {
//--1
new MyThread().start();
//--2
new Thread(new MyRun()).start();
//--3
new Thread(() -> {
System.out.println("Hello Lambda!");
}).start();
//--4
Thread t = new Thread(new FutureTask<String>(new MyCall()));
t.start();
//--5
ExecutorService service = Executors.newCachedThreadPool();
service.execute(() -> {
System.out.println("Hello ThreadPool");
});
service.shutdown();
}
- 输出
Hello MyRun!
Hello MyThread!
Hello ThreadPool
Hello Lambda!
Hello MyCall
六、线程的几种常见方法?
- sleep
Sleep,意思就是睡眠,当前线程暂停一段时间让给别的线程去运行。Sleep是怎么复活的?由你的睡眠时间而定,等睡眠到规定的时间自动复活。
一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
static void testSleep() {
new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("A" + i);
try {
Thread.sleep(5000);
//TimeUnit.Milliseconds.sleep(500)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
static void testSleep2() {
new Thread(() -> {
for (int i = 0; i < 2; i++) {
System.out.println("B" + i);
}
}).start();
}
- 输出
A0
B0
B1
A1
A2
- yield
就是当前线程正在执行的时候停止下来进入等待队列,回到等待队列里在系统的调度算 法里头,还是依然有可能把你刚回去的这个线程拿回来继续执行,当然,更大的可能性是把原来等待的那些拿 出一个来执行,所以yield的意思是我让出一下CPU,后面你们能不能抢到那我不管。
一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
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/thread.join(long millis)
意思就是在自己当前线程加入你调用Join的线程,本线程等待。等调用的线程运行 完了,自己再去执行。t1和t2两个线程,在t1的某个点上调用了t2.join,它会跑到t2去运行,t1等待t2运 行完毕继续t1运行(自己join自己没有意义)。
当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。
static void testJoin() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("T1 " + i);
try {
Thread.sleep(500); //TimeUnit.Milliseconds.sleep(500)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("T2 " + i);
try {
Thread.sleep(500); //TimeUnit.Milliseconds.sleep(500)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
- 输出
T1 0
T1 1
T1 2
T1 3
T1 4
T2 0
T2 1
T2 2
T2 3
T2 4
- obj.wait()
当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
- obj.notify()
唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
- LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines)
当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。
七、线程的状态有几种?
- 新建状态
当我们new一个线程时,还没有调用start()该线程处于新建状态
- Ready就绪状态/Running运行状态
线程对象调用 start()方法时候,他会被线程调度器来执行,也就是交给操作系统来执行了,那么操作系统来执行的时候,这整个的状态叫Runnable,Runnable内部有两个状态(1)Ready就绪状态/(2)Running运行状态。就绪状态是说扔到CPU的等待队列里面去排队等待CPU运行,等真正扔到
CPU上去运行的时候才叫Running运行状态。(调用yiled时候会从Running状态跑到Ready状态去,线程配调度器选中执行的时候又从Ready状态跑到Running状态去)
- Teminated结束状态
线程顺利的执行完了就会进去Teminated结束状态,(需要注意Teminated完了之后还可不可以回到new状态再调用start?这是不行的,完了这就是结束了)
- Blocked阻塞状态
在同步代码块的情况就下没得到锁就会阻塞状态,获得锁的时候是就绪状态运行。
- Waiting等待状态
在运行的时候如果调用了o.wait()、t.join()、LockSupport.park()进入Waiting状态,调用o.notify()、o.notifiAll()、LockSupport.unpark()就又回到Running状态。
- TimedWaiting等待状态
TimedWaiting按照时间等待,等时间结束自己就回去了,Thread.sleep(time)、o.wait(time)、t.jion(time)、LockSupport.parkNanos()、LockSupport.parkUntil()这些都是关于时间等待的方法。
问题1:哪些是JVM管理的?哪些是操作系统管理的?
上面这些状态全是由JVM管理的,因为JVM管理的时候也要通过操作系统,所以呢,那个是操作系统和
那个是JVM他俩分不开,JVM是跑在操作系统上的一个普通程序
问题2:线程什么状态时候会被挂起?挂起是否也是一个状态?
Running的时候,在一个cpu上会跑很多个线程,cpu会隔一段时间执行这个线程一下,在隔一段时间执行那个线程一下,这个是cpu内部的一个调度,把这个状态线程扔出去,从running扔回去就叫线程被挂起,cpu控制它。
问题3:怎么样得到这个线程的状态呢?
通过getState()这个方法获取
八、关闭线程的正确方式
让线程正常结束,不建议使用stop()方法。
问题1. stop() 和 suspend() 方法为何不推荐使用?
- 反对使用 stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。
- suspend() 方法容易发生死锁。调用 suspend() 的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被 “挂 起” 的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用 suspend(),而应在自己的 Thread 类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait() 命其进入等待状态。若标志指出线程应当恢复,则用一个 notify() 重新启动线程。
问题2. 如何停止一个线程
- 1.使用退出标志使线程正常退出
在代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
- 2.使用stop方法不过该方法已经被标记为过时的方法(因为会造成死锁)
- 3.使用interrupt()方法中断线程
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
if (Thread.interrupted()) {
System.out.println("线程被停止了,我要退出");
try {
throw new InterruptedException();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("线程已经被停止了");
}
}
}
}, "");
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) { e.printStackTrace(); }
thread.interrupt();
}
}
九、synchronized关键字
1.锁可以锁任意对象
- 这里的锁作用于O对象
public class test {
private int count = 10;
private Object o = new Object();
public void m() {
synchronized (o) { //任何线程要想执行下面的代码,必须先拿到o的锁
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
public static void main(String[] args) {
new test().m();
}
}
2.锁可以锁定当前对象
- 通过synchronized(this)
public class test {
private int count = 10;
public void m() {
synchronized (this) { //任何线程想要执行那个下面的代码,必须先要拿到this的锁
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
public static void main(String[] args) {
new test().m();
}
}
- 如果你要是锁定当前对象呢,你也可以写成如下方法。synchronized方法和synchronized(this)执行这段代码它是等值的
public class test {
private int count = 10;
public synchronized void m() { //等同于在方法的代码执行时要synchronized(this)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
new test().m();
}
}
3.锁可以作用于静态方法
- 我们知道静态方法static是没有this对象的,你不需要new出一个对象来就能执行这个方法,但如果这个方法上面加一个synchronized的话就代表synchronized(test.class)。这里这个synchronized(test.class)锁的就是test类的对象
public class test {
private static int count = 10;
public synchronized static void m() {//这里等同于synchronized(T.class)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void mm() {
synchronized (test.class) {
count--;
}
}
}
问题:test.class是单例的吗?
一个class load到内存它是不是单例的,想想看。一般情况下是,如果是在同一个ClassLoader空间那它一定是。不是同一个类加载器就不是了,不同的类加载器互相之间也不能访问。所以说你能访问它,那他一定就是单例。
练习
如果有一个线程把它从10减到9了,然后又有一个线程在前面一个线程还没有输出呢进来了把9又减到了8,继续输出的8,而不是9。如果你想修正它,可以第一个是在上面加volatile,还可以synchronized,加了synchronized就没有必要在加volatile了,因为synchronized既保证了原子性,又保证了可见性。
public class test implements Runnable{
private /*volatile*/ int count = 100;
@Override
public /*synchronized*/ void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
test t = new test();
for (int i = 0; i < 10; i++) {
new Thread(t, "THREAD" + i).start();
}
}
}
- 输出
THREAD0 count = 98
THREAD5 count = 97
THREAD2 count = 98
THREAD4 count = 95
THREAD9 count = 96
THREAD8 count = 94
THREAD6 count = 93
THREAD1 count = 92
THREAD3 count = 91
4. 同步方法和非同步方法是否可以同时调用
可以
- 列子
比如说:张三,给他设置100块钱启动了,睡了1毫秒之后去读它的值,然后再睡2秒再去读它的值这个时候你会看到读到的值有问题,原因是在设定的过程中this.name你中间睡了一下,这个过程当中我模拟了一个线程来读,这个时候调用的是getBalance方法,而调用这个方法的时候是不用加锁的,所以说我不需要等你整个过程执行完就可以读到你中间结果产生的内存,这个现象就叫做脏读。这问题的产生就是synchronized方法和非synchronized方法是同时运行的。解决就是把getBalance加上
synchronized就可以了,如果你的业务允许脏读,就可以不用加锁,加锁之后的效率低下。
public class test implements Runnable {
String name;
double balance;
public double getBalance() {
return balance;
}
public synchronized void setBalance(String name,double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
@Override
public void run() {
}
public static void main(String[] args) {
test a = new test();
new Thread(() -> a.setBalance("zhangsan", 100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance());
}
}
5.可重入
- 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。
如果是一个同步方法调用另外一个同步方法,有一个方法加了锁,另外一个方法也需要加锁,加的是同
一把锁也是同一个线程,那这个时候申请仍然会得到该对象的锁。比如说是synchronized可重入的,有
一个方法m1是synchronized,有一个方法m2也是synchrionzed,m1里能不能调m2。我们m1开始的时
候这个线程得到了这把锁,然后在m1里面调用m2,如果说这个时候不允许任何线程再来拿这把锁的时
候就死锁了。这个时候调m2它发现是同一个线程,因为你m2也需要申请这把锁,它发现是同一个线程
申请的这把锁,允许,可以没问题,这就叫可重入锁。
- 所谓的重入锁就是你拿到这把锁之后不停加锁加锁,加好几道,但锁定的还是同一个对象,去一道就减个1,就是这么个概念。
- 列子
public class test {
synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end");
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
public static void main(String[] args) {
new test().m1();
}
}
- 输出
m1 start
m2
m1 end
6.异常锁
-
程序在执行过程中,如果出现异常,默认情况锁会被释放。所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
因此要非常小心的处理同步业务逻辑中的异常。 -
举个栗子
/**
* 看这个小程序,加了锁synchronized void m(),while(true)不断执行,线程启动,count++ 如果等于5的
* 时候认为的产生异常。这时候如果产生任何异常,就会出现什么情况呢? 就会被原来的那些个准备拿到
* 这把锁的程序乱冲进来,程序乱入。这是异常的概念。
*/
public class test {
int count = 0;
synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start");
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
int i = 1 / 0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行 catch,然后让循环继续
System.out.println(i);
}
}
}
public static void main(String[] args) {
test t = new test();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
- 输出
t1 start
t1 count = 1
t1 count = 2
t1 count = 3
t1 count = 4
t1 count = 5
t2 start
t2 count = 6
Exception in thread “t1” java.lang.ArithmeticException: / by zero
at com.jiakai.damaiwang.test.m(test.java:31)
at com.jiakai.damaiwang.test$1.run(test.java:44)
at java.lang.Thread.run(Thread.java:748)
t2 count = 7
t2 count = 8
t2 count = 9
t2 count = 10 …
7.synchronized的底层实现
-
早期,jdk早期的时候,这个synchronized的底层实现是重量级的,重量级到这个synchronized都
是要去找操作系统去申请锁的地步,这就会造成synchronized效率非常低,java后来越来越开始
处理高并发的程序的时候,很多程序员都不满意,说这个synchrionized用的太重了,我没办法,
就要开发新的框架,不用你原生的了。 -
改进,后来的改进才有了锁升级的概念。
-
锁升级的概念
原来,都要去找操作系统,要找内核去申请这把锁,到后期做了对synchronized的一些改进,他的效率比原来要改变了不少,改进的地方。当我们使用synchronized的时候HotSpot的实现是这样的:上来之后第一个去访问某把锁的线程 比如sync (Object) ,来了之后先在这个Object的头上面markword记录这个线程。(如果只有第一个线程访问的时候实际上是没有给这个Object加锁的,在内部实现的时候,只是记录这个线程的ID(偏向锁))。偏向锁如果有线程争用的话,就升级为自旋锁,概念就是(有一个哥们儿在蹲马桶 ,另外来了一个哥们,他就在旁边儿等着,他不会跑到cpu的就绪队列里去,而就在这等着占用cpu,用一个while的循环在这儿转圈玩儿, 很多圈之后不行的话就再一次进行升级)。自旋锁转圈十次之后,升级为重量级锁,重量级锁就是去操作系统那里去申请资源。这是一个锁升级的过程。
需要注意并不是CAS的效率就一定比系统锁要高,这个要区分实际情况
- 执行时间短(加锁代码),线程数少,用自旋
- 执行时间长,线程数多,用系统锁
8.synchronized(Object) 不能用String常量 Integer Long
十、并发编程三要素?
- 原子性:指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
- 可见性:指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
- 有序性:即程序的执行顺序按照代码的先后顺序来执行。
十一、如何优雅的设置睡眠时间?
- TimeUnit.HOURS.sleep(3);
- TimeUnit.MINUTES.sleep(22);
- TimeUnit.SECONDS.sleep(55);
- TimeUnit.MILLISECONDS.sleep(899);