Java高并发编程基础
注:该篇文章已与我的个人博客同步更新。欢迎移步https://cqh-i.github.io/体验更好的阅读效果。进程进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。线程线程是指进程中的一个执行路径,一个进程中可以运行多个线程。同一类的线程共享代码和数据空间, 每个线程使用...
注:该篇文章已与我的个人博客同步更新。欢迎移步https://cqh-i.github.io/体验更好的阅读效果。
进程
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程
线程是指进程中的一个执行路径,一个进程中可以运行多个线程。同一类的线程共享代码和数据空间, 每个线程使用其所属进程的栈空间。线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
进程和线程的区别
主要区别是,同一进程内的多个线程会共享部分状态,多个线程可以读写同一块内存,一个进程无法直接访问另一进程的内存。同时,每个线程还拥有自己的寄存器和栈,它的兄弟线程可以读写这些栈内存。可以表现在以下几方面上:
1.地址空间和其他资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其他进程内不可见。
2.通信:进程间通信IPC包括管道,信号量,共享内存,消息队列,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要线程同步和互斥手段的辅助,以保证数据的一致性。
3.调度和切换:线程上下文切换比进程上下文切换快得多。
Java synchronized(对象锁)和synchronized static(类锁)
-
对象锁: synchronized是对类的当前实例(当前对象)进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块(不包括synchronized static 修饰的方法)。
-
类锁: 由于一个类不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被申明为synchronized,此类所有的实例化对象在调用此方法,共用同一把锁,我们称之为类锁。synchronized static 是限制多线程中该类的所有实例同时访问 JVM 中该类所对应的代码块(synchronized static 修饰的代码),锁在该类的class对象(
xxx.class
)。类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。
Java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个Java对象,只不过有点特殊而已。由于每个Java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。
实际上,在类中如果某方法或某代码块中有 synchronized,那么在生成一个该类实例后,该实例也就有一个监视块,防止线程并发访问该实例的synchronized保护块,而 synchronized static则是该类的所有实例公用的一个监视块,这就是他们两个的区别。
对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。
下面通过例子来加深理解
public class ThreadDemo {
public synchronized void syncM1() {
System.out.println(Thread.currentThread().getName() + " syncM1()方法开始执行, 将休眠5秒 ");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("休眠结束,syncM1()结束");
}
public synchronized void syncM2() {
System.out.println(Thread.currentThread().getName() + " syncM2()方法执行了");
}
public synchronized static void syncSM3() {
System.out.println(Thread.currentThread().getName() + " syncSM3()方法执行了, 将休眠5秒");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("休眠结束,syncSM3()结束");
}
public synchronized static void syncSM4() {
System.out.println(Thread.currentThread().getName() + " syncSM4()方法执行了, 将休眠5秒");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("休眠结束,syncSM4()结束");
}
public void m5() {
System.out.println(Thread.currentThread().getName() + " m5()方法执行了");
}
}
ThreadDemo
有两个synchronized
修饰的方法syncM1()
和syncM2()
, 两个synchronized static
修饰的方法syncSM3
和syncSM4
以及一个普通方法m5()
, 那么,假如有ThreadDemo
类的两个实例x与y,那么下列各组方法被多线程同时访问的情况是怎样的?
-
x.syncM1()
和x.syncM2
public static void main(String[] args) { ThreadDemo x = new ThreadDemo(); Thread t1 = new Thread(() -> x.syncM1(), "t1"); Thread t2 = new Thread(() -> x.syncM2(), "t2"); t1.start(); t2.start(); /* * new Thread(() -> x.syncM1(), "t1"); *相当于是下面的写法,上面这种写法是lamada表达式,Java8以后的特性 * new Thread(new Runnable(){ * public void run(){ * x.syncM1(); * } * }, "t1"); */ }
运行结果:
t1 syncM1()方法开始执行, 将休眠5秒 休眠结束,syncM1()结束 t2 syncM2()方法执行了
运行结果说明了,
x.syncM1()
和x.syncM2
不能被同时访问, 因为都是对同一个实例x的synchronized域访问, 多线程中访问x的不同synchronized域不能同时访问, 必须等待锁的释放。 -
x.syncM1()
和x.syncM1
public static void main(String[] args) { ThreadDemo x = new ThreadDemo(); Thread t1 = new Thread(() -> x.syncM1(), "t1"); Thread t2 = new Thread(() -> x.syncM1(), "t2"); t1.start(); t2.start(); }
运行结果:
t1 syncM1()方法开始执行, 将休眠5秒 休眠结束,syncM1()结束 t2 syncM1()方法开始执行, 将休眠5秒 休眠结束,syncM1()结束
运行结果说明了,在多个线程中访问
x.syncM1()
,因为仍然是对同一个实例,且对同一个方法加锁,所以多个线程中也不能同时访问。(多线程中访问x的同一个synchronized域不能同时访问) -
x.syncM1()
与y.syncM1()
运行结果:
t1 syncM1()方法开始执行, 将休眠5秒
t2 syncM1()方法开始执行, 将休眠5秒
休眠结束,syncM1()结束
休眠结束,syncM1()结束
运行结果说明了,针对不同实例的,可以同时被访问(对象锁对于不同的对象实例没有锁的约束)
-
x.syncSM3()
与y.syncSM4()
public static void main(String[] args) { ThreadDemo x = new ThreadDemo(); ThreadDemo y = new ThreadDemo(); Thread t1 = new Thread(() -> x.syncSM3(), "t1"); Thread t2 = new Thread(() -> y.syncSM4(), "t2"); t1.start(); t2.start(); }
运行结果:
t1 syncSM3()方法执行了, 将休眠5秒 休眠结束,syncSM3()结束 t2 syncSM4()方法执行了, 将休眠5秒 休眠结束,syncSM4()结束
运行结果说明了,不能被同时访问。因为类锁
synchronized static
是限制多线程中该类的所有实例同时访问 JVM 中该类所对应的静态同步代码块。 -
x.syncM1()
与ThreadDemo.syncSM3()
public static void main(String[] args) { ThreadDemo x = new ThreadDemo(); Thread t1 = new Thread(() -> x.syncM1(), "t1"); Thread t2 = new Thread(() -> ThreadDemo.syncSM3(), "t2"); t1.start(); t2.start(); }
运行结果:
t1 syncM1()方法开始执行, 将休眠5秒 t2 syncSM3()方法执行了, 将休眠5秒 休眠结束,syncM1()结束 休眠结束,syncSM3()结束
运行结果说明了,可以被同时访问的,类锁和对象锁控制着不同的区域,它们是互不干扰的。
同步和非同步方法是否可以同时调用
答案是可以被同时调用的。继续使用上面的程序x.syncM1()
和x.m5()
public static void main(String[] args) {
ThreadDemo x = new ThreadDemo();
Thread t1 = new Thread(() -> x.syncM1(), "t1");
Thread t2 = new Thread(() -> x.m5(), "t2");
t1.start();
t2.start();
}
运行结果:
t1 syncM1()方法开始执行, 将休眠5秒
t2 m5()方法执行了
休眠结束,syncM1()结束
一个同步方法可以调用另外一个同步方法吗?
可以。 一个线程已经拥有某个对象的锁,再次申请的时候仍会得到该对象的锁。也就是说synchronized获得的锁是可重入的。
public class ThreadTest {
public synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
}
public synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
public static void main(String[] args) {
ThreadTest t = new ThreadTest();
t.m1();
}
}
运行结果:
m1 start
m2
另外一种情形是子类调用父类的同步方法。
可重入锁(ReentrantLock)原理:
每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
关于生动形象地讲解可重入锁(ReentrantLock)的实现原理可以参考以下博文:
轻松学习java可重入锁(ReentrantLock)的实现原理
线程异常抛出后,锁会被释放吗?
程序在执行过程中,如果出现异常,默认情况锁会被释放。所以,在并发处理过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。
public class T {
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) {
T t = new T();
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();
}
}
正常情况下,线程1死循环,线程2永远不会被执行,但是线程1异常抛出后,锁被释放了,线程2就会执行了。
模拟死锁
死锁出现的条件一般就是线程之间互相等待对方释放锁。比如 线程 1 要先锁定A, 然后再锁定B; 线程2要锁定B, 然后再锁定A,两个线程同时启动,就会出现死锁了。下面是一个例子,创建一个朋友类,当朋友向我们鞠躬的时候,我们也要向朋友鞠躬,这样才算一个完整的动作。当两人同时鞠躬的时候,都在等待对方鞠躬。这时就造成了死锁。
/**
* 死锁模拟程序
*/
public class Deadlock {
/**
* 朋友实体类
*/
static class Friend {
// 朋友名字
private final String name;
// 朋友实体类型的构造方法
public Friend(String name) {
this.name = name;
}
// 获取名字
public String getName() {
return this.name;
}
// 朋友向我鞠躬方法,(同步的)
public synchronized void bow(Friend bower) {
System.out.format("%s: %s" + " has bowed to me!%n", this.name, bower.getName());
bower.bowBack(this);
}
// 我回敬鞠躬方法,(同步的)
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s" + " has bowed back to me!%n", this.name, bower.getName());
}
}
public static void main(String[] args) {
// 死锁模拟程序测试开始
// 创建两个友人alphonse,Gaston
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
// 启动两位友人鞠躬的线程。
new Thread(new Runnable() {
public void run() {
alphonse.bow(gaston);
}
}).start();
new Thread(new Runnable() {
public void run() {
gaston.bow(alphonse);
}
}).start();
}
}
参考:
volatile 关键字
volatile 关键字,使一个变量在多个线程间可见。
A B线程都用到一个变量running,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道。
使用volatile关键字,会让所有线程都会读到变量的修改值。
在下面的代码中,running是存在于堆内存的t对象中。当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy的变量,并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行。使用volatile,每当有一个线程发生对工作区变量的赋值操作,其他线程通过cpu总线嗅探机制,将原来自己工作区的变量失效,然后去主内存(堆内存)中读取新的值。
public class T {
/*volatile*/ boolean running = true;
void m() {
System.out.println("m start");
while (running) {
/*
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
System.out.println("m end!");
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
volatile和synchronized的区别
synchronized可以保证可见性和原子性,volatile只能保证可见性。
public class T {
volatile int count = 0;
/*synchronized*/ void m() {
for (int i = 0; i < 10000; i++)
count++;
}
public static void main(String[] args) {
T t = new T();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
/*
* t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;
* 通常用于在main()主线程内,等待其它线程完成再结束main()主线程
*/
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
上面程序的结果,不是理想的10000,因为count++不是原子操作,可能存在写入脏数据。
更多volatile和synchronized的区别:可参考下面文章
要解决上面这个程序的问题可以使用原子数据类型AtomXXX
类, 它们的increase
之类的操作都是原子操作,它比加synchronized更高效。但是应该注意,执行AtomXXX类的多个方法不构成原子性,虽然它的每个方法都是原子操作,但是在执行完AtomXXX类的上个方法到开始执行AtomXXX类下个方法的这个过程,是有可能被其他线程插入的。
public class T {
/*volatile*/
// int count = 0;
AtomicInteger count = new AtomicInteger(0);
void m() {
for (int i = 0; i < 10000; i++)
// if count.get() < 1000
count.incrementAndGet(); // 替代count++,count++不是原子操作
}
public static void main(String[] args) {
T t = new T();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
synchronized的优化
同步代码块中的语句越少越好,采用细粒度的锁,可以使线程争用时间变短,从而提高效率。
public class T {
int count = 0;
synchronized void m1() {
// do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
count++;
// do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void m2() {
// do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
// 采用细粒度的锁,可以使线程争用时间变短,从而提高效率
synchronized (this) {
count++;
}
// do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
应该避免将锁定对象的引用变成另外的对象
锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变。
public class T {
Object o = new Object();
void m() {
synchronized (o) {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
T t = new T();
// 启动第一个线程
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 创建第二个线程
Thread t2 = new Thread(t::m, "t2");
t.o = new Object(); // 锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
t2.start();
}
}
不要以字符串常量作为锁定对象
在下面的例子中,m1和m2其实锁定的是同一个对象。这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁。
public class T {
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized (s1) {
}
}
void m2() {
synchronized (s2) {
}
}
}
sleep()方法 和 wait()方法有什么区别?
sleep()
是线程类(Thread
)的方法,导致此线程暂停执行指定时间,将执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait()
是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify
方法(或notifyAll
)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)