目录
一、引入
在我有关线程的前两篇博客中详述Java中的线程1——线程与进程 和详述Java中的线程2——线程常用方法 中,我们只研究了不同类线程类个创建一个对象在一个程序中执行时的场景。而真正在写代码时,有可能存在同类线程创建多个对象并发执行的场景,这是就出现了并发。然而并发时,这些同类线程并不会各自运行,而是会交叉执行,这就会使得程序结果不那么有序,为了使输出结果比较“整齐”,势必就要实现多线程的串行。这里我们就要引入线程数据共享(synchronized对象锁)。
package com.thread;
import java.util.Date;
public class Test {
public static void main(String[] args) {
new CounterThread("@@@@@@@@").start(); //开启两个同类不同对象线程
new CounterThread("!!!!!!!!").start();
}
}
class CounterThread extends Thread {
CounterThread(String name) {
super(name);//调用父类有参构造方法,为该线程类起名字
}
public void run() {
int i = 0;
for (; i < 100; i++) {
System.out.println(getName() + new Date());
}
}
}
出现了"很可观"的交叉现象 :
二、synchronized对象锁
synchronized对象锁,顾名思义,它就是一个锁,用于锁住被其包裹的代码,有锁就有钥匙,那钥匙是什么呢?,既然叫对象锁,那这个锁的要是就是一个对象,当然这个钥匙是自定义的,可以是任意类创建的对象,但一旦定义,只有该对象能打开synchronized对象锁。
回到我们的主题,我们想实现同类线程对象的串行不交错,如何实现?
我们不妨把每个线程需要的代码直接都“锁”起来,只配一把“钥匙”,哪个线程抢到“钥匙”,哪个线程执行相应代码。
不难发现,关键在于钥匙的唯一性。
(1)传入实例化对象
package com.thread;
import java.util.Date;
public class Test {
public static void main(String[] args) {//主线程
Object object = new Object();
new CounterThread("@@@@@@@@",object).start();
new CounterThread("!!!!!!!!",object).start();
}
}
class CounterThread extends Thread {//计数线程
Object obj;
CounterThread(String name,Object obj) {
super(name);
this.obj = obj;
}
public void run() {
int i = 0;
synchronized (obj) {
for (; i < 50; i++) {
System.out.println(getName() + new Date());
}
}
}
}
改写之前代码,将计数线程类中run()方法中的所有代码用synchronized锁住,锁对应的对象时obj参数。
17行,在计数线程类中定义属性object;
19行,定义有参构造方法,将Object类对象赋给创建的线程类对象。
在这之前,我们只为了将钥匙传入synchronized锁做准备。
第8行,我们真正定义的对象在主线程中;因为该对象在堆内存中的地址是唯一的,所以这样保证了“钥匙”的唯一性。
运行结果:
可见修改后的输出结果更加有序。
只有这一段代码不能说明synchronized锁的问题,接下来再来一段代码示例,
代码判读:
package com.thread;
import java.text.*;
import java.util.Date;
public class Test {//主线程
public static void main(String[] args) {// a.主线程开始
Object lockObj = new Object();// b.创建对象,作为synchronized锁的钥匙
new DisplayThread(lockObj).start();// c.开启显示器线程
}
}
class DisplayThread extends Thread {//显示器线程
Object lockObj;
public DisplayThread(Object lockObj) {
this.lockObj = lockObj;//创建有参构造方法以传入object对象
}
@Override
public void run() {// d.进行到这里,主线程已终止,显示器线程开始执行run方法,而且目前只有显示器线程这一个线程
synchronized (lockObj) {// e.因为这时显示器线程没有“竞争对手”,所以一定是它先解锁,开始执行解锁的代码
new TimeThread(lockObj).start();// f.进行到这里时间线程开启,也就是此时“钥匙”(object对象)依旧被显示器线程占用,也就说明时间线程必须等到显示器线程终止才能解锁
try {
sleep(60000);// g.这时显示器线程进入60秒的阻塞状态,但object类对象依旧被占用,60秒后,显示器线程终止
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class TimeThread extends Thread {//时间线程
Object lockObj;
public TimeThread(Object lockObj) {
this.lockObj = lockObj;
}
@Override
public void run() {
System.out.println("时间线程开始执行......");//这行输出语句未被synchronized锁住,所以可以正常执行。
synchronized (lockObj) {// h.显示器线程终止后,时间线程被解锁
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
String time = dateFormat.format(new Date());
System.out.println(time);// i.输出时间
}
}
}
也就是说,从第45行语句执行结束到第49行语句执行结束,总共要经过60秒左右。
反例:
package com.thread;
public class Test {
public static void main(String[] args) {
new CounterThread("线程1").start();
new CounterThread("线程2").start();
}
}
class CounterThread extends Thread{
public CounterThread(String threadName){
super(threadName);
}
@Override
public void run() {
synchronized (this) {//此时临界区中的代码无法实现串行执行,this会随着调用run方法对象而变化,不具有唯一性,因为此时对象锁在线程1和线程2之间不共享
for (int i = 0; i < 3; i++) {
System.out.println(getName() + " : " + i);
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
package com.thread;
public class Test {
public static void main(String[] args) {
new Thread(new CounterThread(),"线程1").start();
new Thread(new CounterThread(),"线程2").start();
}
}
class CounterThread implements Runnable {
@Override
public void run() {
synchronized (this) {// 此时临界区中的代码依然无法实现串行执行,因为每一个独立线程拥有一个独立的锁对象——new CounterThread()。
//要明白这两点:
//谁调用该run方法?——CounterThread类对象;
//谁执行该run方法?——正在执行的线程
Thread thread = Thread.currentThread();
for (int i = 0; i < 3; i++) {
System.out.println(thread.getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
(2)传入其他
我们知道,只要synchronized锁中传入的对象具有唯一性,即可实现多线程的串行,所以接下来是一些非实例化对象但却具有唯一性的对象:
package com.thread;
public class Test {//实例1
public static void main(String[] args) {
new CountThread().start();
new CountThread().start();
}
}
class CountThread extends Thread{
@Override
public void run() {
synchronized ("Tom") {//这里传入的是常量池中的字符串,具有唯一性,所以能实现串行化
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + "--->" + i);
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
结果证明了我的观点:
package com.thread;
public class Test {
public static void main(String[] args) {
new CountThread().start();
new CountThread().start();
}
}
class CountThread extends Thread{
@Override
public void run() {
synchronized ((Integer)1) {//该integer类对象依旧来自常量池,也具有唯一性,所以能实现串行化
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + "--->" + i);
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
(3)死锁现象
如果有两个或两个以上的线程都访问了多个资源,而这些线程占用了一些资源的同时又在等待其它线程占用的资源,也就是说多个线程之间都持有了对方所需的资源,而又相互等待对方释放的资源,在这种情况下就会出现死锁。
多个线程互相等待对方释放对象锁,此时就会出现死锁
分析下面一段代码:
public class DeadLockThread {
// 创建两个线程之间竞争使用的对象
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new ShareThread1().start();//线程1开启
new ShareThread2().start();//线程2开启
}
private static class ShareThread1 extends Thread {
public void run() {
synchronized (lock1) {//此时lock1类对象被占用
try {
Thread.sleep(50);//线程1阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {//lock2被线程2占用,线程1再次阻塞
System.out.println("ShareThread1");
}
}
}
}
private static class ShareThread2 extends Thread {
public void run() {
synchronized (lock2) {//此时lock2被占用
try {
Thread.sleep(50);//线程2阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {//lock1被线程1占用,线程2再次阻塞
System.out.println("ShareThread2");
}
}
}
}
}
上面的程序中两个线程占用着对方的需要的解锁对象,就出现了死锁现象。