Java多线程是面试重要考点,其知识面涉及深度和广度都是其他面试题型所不及的,本博客系列《Java多线程与高并发》记录了博主学习高并发与多线程的路径,知识点由浅入深,并附有大量案例程序,可以作为笔记随时翻查。话不多讲,上干货。
程序:是一个静态的实体,是一组有序指令的集合,就是躺在硬盘上的一堆代码文件
进程:程序运行起来,就是一个进程,每个进程占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等
线程:进程里面一个最小的执行单元,一个程序里不同的执行路径
直接调用run是方法调用,调用start是启动一个线程
/**
* thread 基础测试
*
* @author zab
* @date 2019-10-25 22:32
*/
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
static class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
}
上面的例子中,主线程和MyThread类开的线程会同时打印:线程名字+数字,调用的start方法,表示启动一个线程,启动的线程和主线程并行运行。结果如下:
但是如果代码是调用的run方法:
/**
* thread 基础测试
*
* @author zab
* @date 2019-10-25 22:32
*/
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//注意这里如果直接调用run方法,则main方法中就只有一条执行路径
myThread.run();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
static class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
}
输出的结果就是这样的:
可以看到,线程名称都是main,并且都是一个输出完99过后,才是另一个循环,而不是像start方法启动后,两个结果交替输出。
继承Thread,实现Runnable
/**
* 怎样启动一个线程
*
* @author zab
* @date 2019-10-25 22:48
*/
public class HowToStratThread {
public void f(){
System.out.println(Thread.currentThread().getName()+"启动了!");
}
public static void main(String[] args) {
//方式一:继承Thread类,重写run方法
new Thread1().start();
//方式二:实现Runnable接口,重写run方法
new Thread(new Thread2()).start();
//方式三:匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"启动了!");
}
}).start();
//方式四:lambda表达式
new Thread(()->System.out.println(Thread.currentThread().getName()+"启动了!")).start();
//方式五:lambda表达式的特殊写法
HowToStratThread t = new HowToStratThread();
new Thread(t::f).start();
}
static class Thread1 extends Thread{
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"启动了!");
}
}
static class Thread2 implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"启动了!");
}
}
}
可以看到效果:
另外还有线程池ThreadPoolExcutor启动、实现Callable等启动方式,这些个高阶知识,我们后面再聊。
/**
* thread 方法测试
*
* @author zab
* @date 2019-10-25 22:59
*/
public class ThreadMethodTest {
static Thread2 thread2 = new Thread2();
public static void main(String[] args) {
new Thread(new Thread1()).start();
thread2.start();
}
static class Thread1 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行开始!!!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行结束!!!!");
}
}
static class Thread2 extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行开始!");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行结束!");
}
}
}
上面案例简单测试了下join方法,可以看到输出结果是:虽然Thread1只睡了一秒,但是也是等睡了3秒的Thread2执行完了,Thread1才继续往下执行的。
线程状态:当线程类创建对象后,就是new状态,调用方法start()后,就是Runnable状态,可运行态分Ready和Running,正在运行的线程调用yield方法让出cpu,就会从Running转为Ready,其他一些导致线程等待或者阻塞的情况参见上图。
getState()可获取线程状态
加在方法上和锁代码块
public synchronized void f(){
//代码逻辑
}
public void f(){
//代码逻辑
synchronized(o){
//代码逻辑
}
//代码逻辑
}
o不要用String、Integer、Long等基础的对象,因为共享原因可能锁到别人用的值。
锁,锁的是某个对象,拿到某个对象的锁才能执行大括号的代码。
synchronized方法和synchronized(this)等价
同步方法可以调用非同步方法,即synchronized方法中,可以调用非synchronized方法。
我们来看看多线程的情况下,没有同步方法会有什么问题:
import java.util.concurrent.Semaphore;
/**
* synchronized测试
*
* @author zab
* @date 2019-10-25 23:21
*/
public class SyncTest {
static Semaphore semaphore1 = new Semaphore(0);
static Semaphore semaphore2 = new Semaphore(0);
int i = 0;
public void f1(){
for (int j = 0; j < 100000; j++) {
i++;
}
semaphore1.release();
}
public void f2(){
for (int j = 0; j < 100000; j++) {
i++;
}
semaphore2.release();
}
public static void main(String[] args) {
SyncTest t = new SyncTest();
new Thread(t::f1).start();
new Thread(t::f2).start();
try {
semaphore1.acquire();
semaphore2.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.i);
}
}
案例中,每个线程都对SyncTest的公有变量i做十万次的自增操作,但是多次结果输出来却不是二十万。Semaphore是信号量,其中acquire方法表示需要一个许可才能继续往下,release方法则是给当前Semaphore对象释放一个许可。这样保证了只有当两个线程都执行完了,才会打印i的值。
那如果加上synchronized关键字呢???
import java.util.concurrent.Semaphore;
/**
* synchronized测试
*
* @author zab
* @date 2019-10-25 23:21
*/
public class SyncTest {
static Semaphore semaphore1 = new Semaphore(0);
static Semaphore semaphore2 = new Semaphore(0);
int i = 0;
public synchronized void f1(){
for (int j = 0; j < 100000; j++) {
i++;
}
semaphore1.release();
}
public synchronized void f2(){
for (int j = 0; j < 100000; j++) {
i++;
}
semaphore2.release();
}
public static void main(String[] args) {
SyncTest t = new SyncTest();
new Thread(t::f1).start();
new Thread(t::f2).start();
try {
semaphore1.acquire();
semaphore2.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.i);
}
}
为了避免作弊,截图比较全,可以看到真的是二十万。
假如m1是同步的(被synchronized修饰),m2也是同步的
在m1中调用m2,是允许的。这种现象叫锁的可重入,synchronized是可重入锁。
为什么必须是可重入锁?设想父类有个synchronized方法,如果不能重入,子类方法想要继承父类方法是有问题的。
public class Father {
protected synchronized void f() {
System.out.println("我是父亲f方法");
}
public static void main(String[] args) {
Son son = new Son();
son.f();
}
}
class Son extends Father{
@Override
public synchronized void f(){
super.f();
System.out.println("我是儿子f方法");
}
}
程序输出:
我是父亲f方法
我是儿子f方法
Process finished with exit code 0
而实验证明,在子类的synchronized方法中是可以调用父类的synchronized方法的。也就证明了synchronized锁的可重入性。
程序出异常,synchronized锁会被释放
JDK早期1.4之前版本,synchronized是重量级的,每用一次都会向操作系统申请锁。
1.6以上的synchronized锁,效率大大提升,得益于锁的升级过程。
无锁:最开始没有线程访问时,synchronized修饰的方法或者代码块是没有锁的。
偏向锁:一个对象刚开始实例化的时候,没有任何线程来访问它的时候,是可偏向的,意味着它现在认为只可能有一个线程来访问它。所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头MarkWord成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作(高效的原因)。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,操作系统检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁(偏向锁就是这个时候升级为自旋锁)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
自旋锁:如果有多个线程争用,自旋10次,看是否能够获得锁,操作系统用户态解决问题,不是内核态,也叫轻量级锁
重量级锁:向操作系统申请,线程阻塞等待
这个是面试常问的,锁的四种状态??
执行时间短,线程数比较少,自旋锁适合
执行时间长,线程数比较多,重量级锁适合