本文不打算介绍过多多线程的基本知识,旨在总结一下使用多线程中需要注意的东西。
大家都知道要写多线程代码可以实现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的总结,可以把一些零散的知识点汇总在一起,保证看问题的全面性。