Java 线程系列文章只是自己知识的总结梳理,都是最基础的玩意,已经掌握熟练的可以绕过.
一,一个简单的 Demo 引发的血案关于线程同步问题我们从一个简单的 Demo 现象说起.Demo 特别简单就是开启两个线程打印字符串信息.OutPutStr 类源码:
很简单吧,就是一个方法供外界调用,调用的时候传进来一个字符串,方法逐个取出字符串的字符并打印到控制台.接下来,我们看 main 方法中逻辑:
public class OutPutStr {
public void out(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
也很简单,就是开启两个线程分别调用 OutPutStr 中 out 方法不停打印字符串信息,运行程序打印信息如下:
public static void main(String[] args) {
//
final OutPutStr o = new OutPutStr();
new Thread(new Runnable() {@Override public void run() {
//
while (true) {
o.out("111111111111");
}
}
}).start();
new Thread(new Runnable() {@Override public void run() {
//
while (true) {
o.out("222222222222");
}
}
}).start();
}
咦?和我们想的不一样啊,怎么还会打印出 22222222222111111111 这样子的信息,这是怎么回事呢?二,原因解析我们知道线程的执行是 CPU 随机调度的,比如我们开启 10 个线程,这 10 个线程并不是同时执行的,而是 CPU 快速的在这 10 个线程之间切换执行,由于切换速度极快使我们感觉同时执行罢了.发生上面线程的本质就是 CPU 对线程执行的随机调度,比如 A 线程此时正在打印信息还没打印完毕此时 CPU 切换到 B 线程执行了,B 线程执行完了又切换回 A 线程执行就会导致上面现象发生.线程同步问题往往发生在多个线程调用同一方法或者操作同一变量,但是我们要知道其本质就是 CPU 对线程的随机调度,CPU 无法保证一个线程执行完其逻辑才去调用另一个线程执行.三,同步方法解决上述问题既然知道了问题发生的原因,记下来我们就要想办法解决问题啊,解决的思路就是保证一个线程在调用 out 方法的时候如果没执行完那么另一个不能执行此方法,换句话说就是只能等待别的线程执行完毕才能执行.针对线程同步问题 java 早就有解决方法了,最简单的就是给方法加上 synchronized 关键字,如下:
222222222222
222222222222
22222222222111111111
2
111111111111
111111111111
1111222222222211111111
111111111111
这是什么意思呢?加上 synchronized 关键字后,比如 A 线程执行 out 方法就相当于拿到了一把锁,只有获取这个锁才能执行此方法,如果在 A 线程执行 out 方法过程中 B 线程也想插一脚进来执行 out 方法,对不起此时这是不能够的,因为此时锁在 A 线程手里,B 线程无权拿到这把锁,只有等到 A 线程执行完后放弃锁,B 线程才能拿到锁执行 out 方法.为 out 方法加上 synchronized 后其就变成了同步方法,普通同步方法的锁是 this, 也就是当前对象,比如 demo 中,外部要想调用 out 方法就必须创建 OutPutStr 类实例对象 o,此时 out 同步方法的锁就是这个 o.四,同步代码块解决上述问题我们也可以利用同步代码块解决上述问题,修改 out 方法如下:
public synchronized void out(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
同步代码块写法:synchronized(obj){},其中 obj 为锁对象,此处我们传入 this,同样方法的锁也为当前对象,如果此处我们传入 str,那么这里的锁就是 str 对象了.为了说明不同锁带来的影响我们修改 OutPutStr 代码如下:
public void out(String str) {
synchronized(this) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
很简单我们就是加入了一个 out1 方法,out 方法用同步函数保证同步,out1 用同步代码块保证代码块,但是锁我们用的是 str.main 代码:
public class OutPutStr {
public synchronized void out(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
public void out1(String str) {
synchronized(str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
}
也没什么,就是其中一个线程调用 out 方法,另一个调用 out1 方法,运行程序:
public static void main(String[] args) {
//
final OutPutStr o = new OutPutStr();
new Thread(new Runnable() {@Override public void run() {
//
while (true) {
o.out("111111111111");
}
}
}).start();
new Thread(new Runnable() {@Override public void run() {
//
while (true) {
o.out1("222222222222");
}
}
}).start();
}
看到了吧,打印信息又出问题了,就是因为 out 与 out1 方法的锁不一样导致的,线程 A 调用 out 方法拿到 this 这把锁,线程 B 调用 out1 拿到 str 这把锁,二者互不影响,解决办法也很简单,修改 out1 方法如下即可:
111111111111222
222222222222
111111111111222222222222
222222222222
五,静态函数的同步问题我们继续修改 OutPutStr 类,加入 out2 方法:
public void out1(String str) {
synchronized(this) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
main 中两个子线程分别调用 out1,ou2 打印信息,运行程序打印信息如下;
public class OutPutStr {
public synchronized void out(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
public void out1(String str) {
synchronized(this) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
public synchronized static void out2(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
咦?又出错了,out2 与 out 方法唯一不同就是 out2 就是静态方法啊,不是说同步方法锁是 this 吗,是啊,没错,但是静态方法没有对应类的实例对象依然可以调用,那其锁是谁呢?显然静态方法锁不是 this,这里就直说了,是类的字节码对象,类的字节码对象是优先于类实例对象存在的.将 ou1 方法改为如下:
222222222222
222222222222
222222222111111111111
111111111111
再次运行程序,就会发现信息能正常打印了.六,synchronized 同步方式总结到此我们就该小小的总结一下了,普通同步函数的锁是 this,当前类实例对象,同步代码块锁可以自己定义,静态同步函数的锁是类的字节码文件.总结完毕,就是这么简单.说了一大堆理解这一句就够了.七,JDK1.5 中 Lock 锁机制解决线程同步大家是不是觉得上面说的锁这个玩意咋这么抽象,看不见,摸不着的.从 JDK1.5 起我们就可以根据需要显性的获取锁以及释放锁了,这样也更加符合面向对象原则.
public void out1(String str) {
synchronized(OutPutStr.class) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
Lock 接口的实现子类之一 ReentrantLock,翻译过来就是重入锁,就是支持重新进入的锁,该锁能够支持一个线程对资源的重复加锁,也就是说在调用 lock() 方法时,已经获取到锁的线程,能够再次调用 lock() 方法获取锁而不被阻塞,同时还支持获取锁的公平性和非公平性,所谓公平性就是多个线程发起 lock() 请求,先发起的线程优先获取执行权,非公平性就是获取锁与是否优先发起 lock() 操作无关.
接下来我们修改 OutPutStr 类,添加 out3 方法:
关键注释都在代码中有所体现了,使用起来也很简单.
//true表示公平锁,false非公平锁
ReentrantLock lock = new ReentrantLock(true);
public void out3(String str) {
lock.lock(); //如果有其它线程已经获取锁,那么当前线程在此等待直到其它线程释放锁.
try {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
} finally {
lock.unlock(); //释放锁资源,之所以加入try{}finally{}代码块,
//是为了保证锁资源的释放,如果代码发生异常也可以保证锁资源的释放,
//否则其它线程无法拿到锁资源执行业务逻辑,永远处于等待状态.
}
}
关于线程同步问题到这里就结束了,java 多线程文章只是本人工作以来的一次梳理,都比较基础,但是却很重要的,最近招人面试的最大体会就是都喜欢那些所谓时髦的技术一问基础说的乱七八糟,浪费彼此的时间,好啦,吐槽了几句,本文到此为止,很基础的玩意,希望对你有用.
来源: http://www.bubuko.com/infodetail-2464410.html