本文不打算介绍过多多线程的基本知识,旨在总结一下使用多线程中需要注意的东西。
大家都知道要写多线程代码可以实现Runnable接口或者继承Thread类。关于二者的区别,大家都知道java是单继承机制的,继承了Thread可能会让你无法再继承其他父类。可能没有考虑内存相关的问题,导致多线程失效。
直接用代码来讨论,以一个卖票系统为例,总票数15张,模拟3个窗口同时卖票。
实现Runnable
/**
* 不加入同步
* @author
*
*/
public class Ticket1 implements Runnable {
// 总票数
private int total = 15;
public void run() {
while(true) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
public static void main(String[] args) {
Ticket1 t = new Ticket1();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
这段代码的问题很明显,没有加入同步控制,输出大致如下:
t3 sales ticket_15
t2 sales ticket_14
t1 sales ticket_13
t2 sales ticket_12
t3 sales ticket_11
t1 sales ticket_10
t3 sales ticket_9
t1 sales ticket_7
t2 sales ticket_8
t3 sales ticket_6
t1 sales ticket_5
t2 sales ticket_4
t2 sales ticket_3
t3 sales ticket_2
t1 sales ticket_1
t2 sales ticket_0
t3 sales ticket_-1
加入同步控制保证买票正常:
package com.cmsz.upay.thread;
/**
* 加入同步
* @author
*
*/
public class Ticket2 implements Runnable {
// 总票数
private int total = 15;
public void run() {
while(true) {
synchronized (this) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
}
public static void main(String[] args) {
Ticket2 t = new Ticket2();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
这段代码可以保证卖票正常:
t1 sales ticket_15
t1 sales ticket_14
t1 sales ticket_13
t1 sales ticket_12
t1 sales ticket_11
t1 sales ticket_10
t1 sales ticket_9
t1 sales ticket_8
t1 sales ticket_7
t1 sales ticket_6
t1 sales ticket_5
t3 sales ticket_4
t3 sales ticket_3
t3 sales ticket_2
t3 sales ticket_1
继承Thread
先上一段有问题的代码,应该是比较多初学者不会考虑到的:
/**
* 继承thread
* @author
*
*/
public class Ticket3 extends Thread {
// 总票数
private int total = 15;
public void run() {
while(true) {
synchronized (this) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
}
public static void main(String[] args) {
Ticket3 t1 = new Ticket3();
Ticket3 t2 = new Ticket3();
Ticket3 t3 = new Ticket3();
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
运行结果大致如下:
t2 sales ticket_15
t1 sales ticket_15
t3 sales ticket_15
t3 sales ticket_14
t2 sales ticket_14
t1 sales ticket_14
t2 sales ticket_13
t3 sales ticket_13
t1 sales ticket_13
t3 sales ticket_12
t2 sales ticket_12
t1 sales ticket_12
t2 sales ticket_11
t3 sales ticket_11
t1 sales ticket_11
t2 sales ticket_10
t1 sales ticket_10
t3 sales ticket_10
t1 sales ticket_9
t2 sales ticket_9
t3 sales ticket_9
t1 sales ticket_8
t2 sales ticket_8
t3 sales ticket_8
t2 sales ticket_7
t1 sales ticket_7
t3 sales ticket_7
t2 sales ticket_6
t1 sales ticket_6
t3 sales ticket_6
t2 sales ticket_5
t1 sales ticket_5
t3 sales ticket_5
t2 sales ticket_4
t3 sales ticket_4
t1 sales ticket_4
t2 sales ticket_3
t3 sales ticket_3
t1 sales ticket_3
t2 sales ticket_2
t1 sales ticket_2
t3 sales ticket_2
t2 sales ticket_1
t3 sales ticket_1
t1 sales ticket_1
很明显每个线程都卖了15张票,跟我们预期的不符合,我们在学习面向对象的时候知道了两个概念:类变量和实例变量,不展开细说二者的区别,实例变量就是每new一个实例出来之后,变量都会有一个自己的内存区域,不受他人影响;而类变量就是所有通过该类实例化的对象共享一个变量。Ticket3
的问题之处就是将票总数total
声明为实例变量了,这样我们每新建一个实例(线程)total
都会有一个自己的内存区域,所以每次卖的都是自己那15张票。
OK,发现问题解决问题,将票数声明为类变量问题应该就能解决了,试一下:
package com.cmsz.upay.thread;
/**
* 继承thread,共享变量
* @author
*
*/
public class Ticket4 extends Thread {
// 总票数
private static int total = 15;
public void run() {
while(true) {
synchronized (this) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
}
public static void main(String[] args) {
Ticket4 t1 = new Ticket4();
Ticket4 t2 = new Ticket4();
Ticket4 t3 = new Ticket4();
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
运行结果如下:
t2 sales ticket_15
t1 sales ticket_14
t3 sales ticket_13
t1 sales ticket_12
t2 sales ticket_11
t3 sales ticket_10
t2 sales ticket_9
t1 sales ticket_8
t3 sales ticket_7
t2 sales ticket_5
t1 sales ticket_6
t3 sales ticket_4
t2 sales ticket_3
t1 sales ticket_2
t3 sales ticket_1
t1 sales ticket_0
t2 sales ticket_-1
坑爹的问题出现了,虽然是大家共同卖15张票,但是卖出了0和-1号的票,也就是我们买了17张票,现实是这样的话估计抢座要打起来了……
分析一下原因,共享变量没问题,那就是同步出现问题了,同步有什么问题呢?
问题出现在synchronized(this)
,获取锁的对象是this,很明显3个Thread的this对象是不同的,说白了就是这个加锁根本没有锁住共享变量。
知道了问题的原因,我们只要保证synchronized(syncObject)
中的syncObject
唯一即可,声明一个类变量即可。
/**
* 继承thread,共享变量,锁相同对象
* @author
*
*/
public class Ticket5 extends Thread {
private static Object syncObject = new Object();
// 总票数
private static int total = 15;
public void run() {
while(true) {
synchronized (syncObject) {
if(total > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--);
} else {
break;
}
}
}
}
public static void main(String[] args) {
Ticket5 t1 = new Ticket5();
Ticket5 t2 = new Ticket5();
Ticket5 t3 = new Ticket5();
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
运行即可保证正常。
本文总结主要针对编写多线程代码过程中共享变量的问题和锁机制的一些细节,初学者容易犯错或者欠考虑,通过几个Demo的总结,可以把一些零散的知识点汇总在一起,保证看问题的全面性。