原因是,你想拿到一个更高的薪水,在面试的时候呈现出了两个方向的现象:
第一个上天
第二个入地
多线程和高并发,就是入地里面的内容。
我们先从线程的基本概念开始,给大家复习一下。如果基础不太好,说什么是线程都不知道的,则需要花时间去补初级内容的课。
1. 什么是进程?什么是线程?
进程:做一个简单的解释,你的硬盘上有一个简单的程序,这个程序叫QQ.exe,这是一个程序,这个程序是一个静态的概念,它被扔在硬盘上也没人理它;但是当你双击它时,会弹出一个界面输入账号密码登录进去了,OK,这个时候叫做一个进程。进程相对于程序来说它是一个动态的概念。
线程:作为一个进程里面最小的执行单元它就叫一个线程,用简单的话讲一个程序里不同的执行路径叫做一个线程。
示例:什么叫线程
package com.java.z_exam.juc.c01;
import java.util.concurrent.TimeUnit;
// 多线程与高并发:什么是线程?
public class T01_WhatIsThread {
// 定义一个T1 线程类
private static class T1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException ex){
ex.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(1);
} catch (InterruptedException ex){
ex.printStackTrace();
}
System.out.println("main");
}
}
}
观察上面程序的输出结果,会看到字符串"T1"和"main"的交替输出,这就是程序中有两条不同的执行路径在交叉执行,这就是直观概念上的线程,概念性的东西,理解就好,没有必要咬文嚼字地去背文定的定义。
代码示例:
package com.java.z_exam.juc.c01;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
// 多线程与高并发:创建线程的几种方式
public class T02_HowToCreateThread {
// 方式一:继承自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!");
}
}
static class MyCall implements Callable {
@Override
public String call() throws Exception {
System.out.println("Hello MyCall!");
return "success";
}
}
// 启动线程的5种方式
public static void main(String[] args) {
new MyThread().start();
new Thread(new MyRun()).start();
new Thread( () -> {
System.out.println("Hello Lambda!");
}).start();
Thread t = new Thread(new FutureTask(new MyCall()));
t.start();
ExecutorService service = Executors.newCachedThreadPool();
service.execute( () -> {
System.out.println("Hello ThreadPoll!");
});
service.shutdown();
}
}
分享一道面试题:请你告诉我启动线程的三种方式?
package com.java.z_exam.juc.c01;
// 多线程与高并发:认识几个线程的方法
public class T03_Sleep_Yield_Join {
public static void main(String[] args) {
//testSleep();
//testYield();
testJoin();
}
/*
Sleep,意思就是睡觉,当前线程暂停一段时间让给别的线程去运行。
Sleep是怎么复活的?由你的睡眠时间而定,等睡眠到规定的时间自动复活
*/
static void testSleep() {
new Thread( () -> {
for (int i = 0; i < 100; i++) {
System.out.println("A" + i);
try {
Thread.sleep(500);
} catch (InterruptedException ex){
ex.printStackTrace();
}
}
}).start();
}
/*
Yield,就是当前线程正在执行的时候停止下来进入等待队列,在系统的调度
算法里头,还是依然可能把你刚回去的这个线程拿回来继续执行;
当然,更大的可能性是把原来等待的那些拿出一个来执行,所以yield的意思是我让出一下CPU,
后面你们能不能抢到那我不管
*/
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的线程(),本线程等待。等调用的线程运行
完了,自己再去执行。t1和t2两个线程。在t1的某个点上调用了t2.join,它会跑到t2去运行,
t1等待t2运行完毕继续t1运行(自己join自己没有意义)
*/
static void testJoin() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
System.out.println("A" + i);
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
try {
t1.join(); // 等待t1线程完成,再继续执行
} catch (InterruptedException ex) {
ex.printStackTrace();
}
for (int i = 0; i < 50; i++) {
System.out.println("B" + i);
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
当我们new 一个线程时,还没有调用start() 该线程处于 新建状态。
线程对象调用start() 方法时候,他会被线程调度器来执行,也就是交给操作系统来执行了,那么操作系统来执行的时候,这整个的状态叫Runnable,Runnable 内部有两个状态(1)Ready就绪状态/(2)Running运行状态。就绪状态是说扔到CPU 的等待队列里面去排队等待CPU运行,等真正扔到CPU上去运行的时候才叫Running 运行状态。(调用yield 时候会从Running状态变为Ready状态, 线程调度器选中执行的时候又从Ready 状态变为Running 状态了)。
如果线程顺利地执行完了就会进入(3)Terminated 结束状态,(需要注意Teminated 完了之后还可不可以回到new 状态再调用start 方法?这是不行的,完了就是结束了)。
在Runnable 这个状态里头还有其他一些状态的变迁(4)TimedWaiting 等待、(5)Waiting 等待、(6)Blocked 阻塞,在同步代码块的情况就下没得到锁就会进入阻塞状态,获得锁的时候是继续运行。在运行的时候如果调用了o.wait()、t.join()、LockSupport.park() 进入Waiting状态,调用o.notify()、o.notifiAll()、LockSupport.unpark() 就又回到Running 状态。TimedWaiting按照时间等待,等时间结束自己就回去了,Thread.sleep(time)、o.wait(time)、t.join(time)、LockSupport.parkNanos()、LockSupport.parkUntil() 这些都是关于时间等待的方法。
问题1:哪些是JVM管理的?哪些是操作系统管理的?
上面这些状态全是由JVM管理的,因为JVM管理的时候也要通过操作系统,所以呢,哪个是操作系统和哪个是JVM他俩分不开,JVM是跑在操作系统上的一个普通程序。
问题2:线程什么状态时候会被挂起?挂起是否也是一个状态?
Running 的时候,在一个CPU 上会跑多个线程,CPU会隔一段时间执行这个线程一下,再隔一段时间执行那个线程一下,这个是CPU内部的一个调度,把这个状态线程扔出去,从running 扔回去就叫线程被挂起,CPU控制它。
代码示例:
package com.java.z_exam.juc.c01;
// 多线程与高并发:常见的线程状态
public class T04_ThreadState {
static class MyThread extends Thread {
@Override
public void run() {
System.out.println(this.getState());
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException ex){
ex.printStackTrace();
}
System.out.println(i);
}
}
}
public static void main(String[] args) {
MyThread t = new MyThread();
// 怎么样得到这个线程的状态呢?就是通过getState() 这个办法
System.out.println(t.getState()); // 它是一个new 状态
t.start(); // 到这个start完了之后,是Runnable 的状态
try {
t.join();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
// 然后join 之后,结束了是一个Terminated 状态
System.out.println(t.getState());
}
}
多个线程去访问同一个资源的时候对这个资源上锁。
什么要上锁呢?访问某一段代码或者临界资源的时候是需要有一把锁的概念在这儿的。比如 :我们对一个数字做递增,两个程序对它一块儿来做递装置,递增就是把一个程序往上加1,如果两个线程共同访问的时候,第一个线程一读它是0,然后把它加1,在自己线程内部内存里面算还没有写回去的时候而第二个线程读到了它还是0,加1再写回去,本来加了两次,但还是1,那么我们在对这个数字递增的过程当中就上把锁,就是说第一个线程对这个数字访问的时候是独占的,不允许别的线程来访问,不允许别的线程来对它计算,我必须加完1后释放锁,其他线程才能对它继续加锁。
实质上,这把锁并不是对数字进行锁定的,你可以任意指定,相锁谁就锁谁。
下面是一个小程序是这么写的,如果说你想上了把锁之后才能对count 进行减减访问,你可以new 一个Object,所以这里锁定就是o,当我拿到这把锁的时候才能执行这段代码。是锁定的某一个对象,synchronized 有一个锁升级的概念,后续还会再次提到它。
package com.java.z_exam.juc.c02_synchronized;
import java.util.Vector;
// synchronized 关键字:对某个对象加锁
public class T01 {
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) throws InterruptedException {
T01 t01 = new T01();
Vector vector = new Vector();
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(() -> {
t01.m();
});
vector.add(thread);
thread.start();
}
// 等待所有工作线程执行完成
for (int i = 0; i < vector.size(); i++) {
Thread t = (Thread)vector.get(i);
t.join();
}
System.out.println("All thread run finished!");
}
}
再来聊聊synchronized 它的一些特性。如果说你每次都定义一个锁的对象Object o 把它new 出来那加锁的时候太麻烦每次都要new 一个新的对象出来,所以呢,有一个简单的方式就是synchronized(this) 锁定当前对象就行。
package com.java.z_exam.juc.c02_synchronized;
import java.util.Vector;
// synchronized 关键字:对某个对象加锁
public class T02 {
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) throws InterruptedException {
T02 t01 = new T02();
Vector vector = new Vector();
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(() -> {
t01.m();
});
vector.add(thread);
thread.start();
}
// 等待所有工作线程执行完成
for (int i = 0; i < vector.size(); i++) {
Thread t = (Thread)vector.get(i);
t.join();
}
System.out.println("All thread run finished!");
}
}
如果你想要锁定当前对象呢,你也可以写成如下方法。synchronized 方法和 synchronized(this) 执行这段代码它是等值的。
package com.java.z_exam.juc.c02_synchronized;
public class T03 {
private int count = 10;
public synchronized void m() { // 等同于在方法的代码执行时要synchronized(this)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
我们知道静态方法static 是没有this 对象的,你不需要new 出一个对象来就能执行这个方法;但如果这个上面加一个synchronized 的话就代表 synchronized(T.class)。这里这个synchronized(T.class) 锁的就是T类的对象。
package com.java.z_exam.juc.c02_synchronized;
public class T04 {
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 m2() {
synchronized (T04.class) { // 考虑一下这里写 synchronized(this) 是否可以?
count--;
}
}
}
问题:T.class 是单例的吗?
一个class load 到内存它是不是单例的,想想看。一般情况下是,如果是在同一个ClassLoader 空间那它一定是。不是同一个类加载器就不是了,不同的类加载器互相之间也不能访问。所以说你能访问它,那它一定就是单例。
下面程序:很大可能读不到别的线程修改过的内容,除了这点之外 count 减减完了之后下面的count 输出和你减完的结果不对。很容易分析:如果有一个线程把它从10减到9了,然后又有一个线程在前面一个线程还没有输出呢进来了把9 又减到8,继续输出的8,而不是9。如果你想修正它,前面第一个是在上面加volatile,改了马上就能得到。
package com.java.z_exam.juc.c02_synchronized;
import java.util.ArrayList;
import java.util.List;
public class T05 implements Runnable {
private /*volatile*/ int count = 100; // volatile 保证多线程可见性
@Override
public /*synchronized*/ void run() { // 加锁,序列化执行,性能有下降
count--;
//System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
T05 t = new T05();
System.out.println("Start:" + t.count);
List list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(t, "Thread:" + i);
list.add(thread);
thread.start();
}
// 等待所有工作线程执行完成
for (int i = 0; i < list.size(); i++) {
list.get(i).join();
}
long end = System.currentTimeMillis();
System.out.println("Finish:" + t.count + " spend:" + (end - start));
}
}
另外这个之外还可以加synchronized,加了synchronized 就没有必要再加volatile了,因为synchronized 既保证了原子性,又保证了可见性。
同步方法和非同步方法是否可以同时调用?
就是我有一个synchronized 的m1 方法,我调用m1的时候能不能调用m2 ?拿大腿想一想这个是肯定可以的,线程里面访问m1 的时候需要加锁,可以访问m2 的时候我又不需要加锁,所以允许执行m2。这些小实验的设计是比较考验功力的,学习线程的时候自己要多动手进行试验,任何一下理论,都可以进行验证。
package com.java.z_exam.juc.c02_synchronized;
// 同步和非同步方法是否同时调用?
public class T07 {
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start...");
try {
Thread.sleep(10000);
} catch (Exception ex){
ex.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end");
}
public void m2() {
try {
Thread.sleep(5000);
} catch (Exception ex){
ex.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m2 ");
}
/*new Thread( () -> t.m1(), "t1").start();
new Thread( () -> t.m2(), "t2").start();*/
public static void main(String[] args) {
T07 t = new T07();
new Thread(t::m1, "t1").start();
new Thread(t::m2, "t2").start();
/*new Thread(new Runnable() {
@Override
public void run() {
t.m1();
}
});*/
}
}
运行结果截图:
我们再来看一个synchronized 应用的例了。
我们定义了一个class 账户,有名称、金额。写方法给哪个用户设置它多少金额,读方法通过这个名字得到余额值。如果我们给写方法加锁,给读方法不加锁,你的业务允许产生这种问题吗?业务说我中间读到了一些不太好的数据也没关系,如果不允许客户读到中间不好的数据那这个就有问题。正因为我们加了锁的方法和不加锁的方法可以同时运行。
问题示例:
张三,给他设置100块钱启动了,睡了1毫秒之后呢去读它的值,然后再睡2秒再去读它的值这个时候你会看到读到的值有问题 ,原因是在设定的过程中 this.name 你中间睡了一下,这个过程当中我模拟了一个线程来读,这个时候调用的是getBlance 方法,而调用这个方法的时候是不用加锁的,所以说我不需要等你整个过程执行完就可以读到你中间结果产生的内存,这个现象叫做脏读。这个问题的产生就是synchronized 方法和非synchronized 方法是同时运行的。解决就是把getBlance 加上synchronized 就可以了,如果你的业务允许脏读,就可以不用加锁,加锁之后的效率低下。
package com.java.z_exam.juc.c02_synchronized;
import java.util.concurrent.TimeUnit;
/*
面试题:模拟银行账户
对业务写方法加锁
对业务读方法不加锁
这样行不行?
容易产生脏读问题(dirtyRead)
*/
public class T08 {
String name;
double balance;
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (Exception ex){
ex.printStackTrace();
}
this.balance = balance;
}
public /*synchronized*/ double getBalance(String name) {
return this.balance;
}
public static void main(String[] args) {
T08 a = new T08();
new Thread( () -> a.set("zhangsan", 100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception ex){
ex.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception ex){
ex.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
}
再来看synchronized 的另外一个属性:可重入,是synchronized 必须了解的一个概念。
如果是一个同步方法调用另外一个同步方法,有一个方法加了锁,另外一个方法也需要加锁,加的是同一把锁也是同一个线程,那这个时候申请仍然会得到该对象的锁。比如说是synchronized 可重入的,有一个方法m1 是synchronized 有一个方法m2 也有synchronized,m1 里能不能调m2 ?我们m1 开始的时候这个线程得到了这把锁,然后在m1 里面调用m2,如果说这个时候不允许任何线程再来拿这把锁的时候就死锁了。这个时候调m2 它发现是同一个线程,因为你m2 也需要申请这把锁,它发现是同一个线程申请的这把锁,允许,可以没问题,这就叫可重入锁。
package com.java.z_exam.juc.c02_synchronized;
import java.util.concurrent.TimeUnit;
/*
一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。
也就是说synchronized 获得锁是可重入的
*/
public class T09 {
synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception ex){
ex.printStackTrace();
}
m2();
System.out.println("m1 end");
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception ex){
ex.printStackTrace();
}
System.out.println("m2");
}
public static void main(String[] args) {
new T09().m1();
}
}
模拟一个父类子类的概念,父类synchronized,子类调用super.m 的时候必须得可重入,否则就会出问题(调用父类是同一把锁)。所谓的重入锁就是你拿到这把锁之后不停加锁加锁,加好几道,但锁定的还是同一个对象,去一道就减个1,就是这么个概念。
package com.java.z_exam.juc.c02_synchronized;
import java.util.concurrent.TimeUnit;
public class T10 {
synchronized void m() {
System.out.println("m start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e){
e.printStackTrace();
}
System.out.println("m end");
}
public static void main(String[] args) {
new TT().m();
}
}
class TT extends T10 {
@Override
synchronized void m() {
System.out.println("child m start");
super.m();
System.out.println("child m end");
}
}
下面再看,异常锁。
看这个小程序,加了锁synchronized void m(),while(true) 不断执行,线程启动,count++ 如果等于5的时候认为的产生异常。这时候如果产生任何异常,就会出现什么情况呢?就会被原来的那些准备拿到这把锁的程序乱冲进来,程序乱入。这是异常的概念。
package com.java.z_exam.juc.c02_synchronized;
import java.util.concurrent.TimeUnit;
/*
程序在执行过程中,如果出现异常,默认情况锁会被释放
所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
比如,在一个web app 处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
因此要非常小心的处理同步业务逻辑中的异常
*/
public class T11 {
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 (Exception e){
e.printStackTrace();
}
if (count == 5) {
int i = 1/0; // 此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
System.out.println(i);
}
}
}
public static void main(String[] args) {
T11 t = new T11();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e){
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
参考链接:https://blog.csdn.net/weixin_32265569/article/details/108168792
需要注意并不是CAS的效率就一定比操作系统锁要高,这个要区分实际情况:
内容回顾
文章最后,给大家推荐一些受欢迎的技术博客链接:
欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!