实现线程的有继承 thread类和实现runnable接口两种方式,一般没人会说实现callable接口这个方式,所以,这就暂不考虑这个方法。
下面分别以这2种方式实现线程安全的卖票的例子。
1,继承thread类来实现多线程卖票。
先是票的代码
package com.lxk.threadTest.ticket.extend;
/**
* Created by lxk on 2017/6/25
*/
public class Ticket extends Thread {
//private int ticket = 100;//创建一个对象就有100张票。错误一:几个线程都打印一次100-1。不合适。所以,如下操作,换成静态变量。
private static int ticket = 100;//静态变量是所有对象都共享,100张票。几个线程,卖的都是一个票。但是,一般都不这么干,静态变量,生命周期太长。
/**
* 实现自定义线程的名称
*/
public Ticket(String name) {
super(name);
}
/**
* 这地方就是需要注意的地方,如果不加[synchronized],就会发生线程安全问题。
* 奇怪了,
* 怎么还是线程不安全,还是会执行出0,-1,-2。的结果出来。
* 错误原因的分析:
* 可以看到添加的锁的对象是this,但是在main方法中有4个对象,每个对象都对自己加锁,锁不同,所以,还是不安全的。
* 比如:换成都对Ticket.class(内存中就有一份字节码文件存在)加锁,那就安全啦。
*/
@Override
//public synchronized void run() {//【①】 错误二:即使添加了同步方法(此处注释的代码),锁的是this,是线程不安全的。
public void run() {
while (true) {
//synchronized (this) {//【①】 错误二:即使添加了同步代码块,锁的也是this,是线程不安全的。
//synchronized (Ticket.class) {//【①】正确同步方式: 必须所有实例化的对象都锁相同的一个家伙,那就安全啦。
if (ticket > 0) {
//睡一下,好实现线程不安全的现象,前提是这个方法,没有添加synchronized,同步函数。
try {
Thread.sleep(10);
} catch (Exception ignored) {
}
//错误三:不添加同步(即注释掉标记:①的所有代码),多线程操作则会打印出0,-1,-2
//分析:
//假设1线程运行到下行代码处,还未执行,此时ticket的值仍然为1,那么其他线程继续执行还是都会进到这个判断
//假设其他几个线程都恰好停到此处,那么依次执行完之后,四个线程的结果就是,1,0,-1,-2.
System.out.println(this.getName() + " sale:" + ticket--);
}
//} //【①】
}
}
}
下面是main方法
package com.lxk.threadTest.ticket.extend;
/**
* 卖票例子(继承Thread类,实现多线程)
*
* Created by lxk on 2017/6/25
*/
public class Main {
public static void main(String[] args) {
Ticket ticket1 = new Ticket("ticket1");
Ticket ticket2 = new Ticket("ticket2");
Ticket ticket3 = new Ticket("ticket3");
Ticket ticket4 = new Ticket("ticket4");
ticket1.start();
ticket2.start();
ticket3.start();
ticket4.start();
}
}
先不要惊慌:我都说了线程安全了,怎么还是输出了0,-1,-2,这些个不合法的票呢。
其实,这是上面ticket类里面的代码把那些同步的代码都给注释了,就是要示范一下,怎么个线程不安全。
也就是带有这个的(//【①】)代码,打开的话(标记过的是错误的就表打开啦 ),就不会看到卖出0,-1,-2的票啦
上面几种情况,我都把详细的注释,写在代码里了。跟着代码走,应该可以很好的理解这个多线程是怎么运行的吧。
这有个前提:就是你知道cpu是轮询执行程序的。这个是最基本的概念啦,每个线程,他是说停就停的,知道这个就好说啦。
2,再是实现runnable接口来实现多线程卖票的实例。
还是先看票的代码
package com.lxk.threadTest.ticket.implement;
/**
* Created by lxk on 2017/6/25
*/
public class Ticket implements Runnable {
private int tick = 100;
boolean flag = true;
//Object object = new Object();
public void run() {
if (flag) {
while (true) {
//synchronized (object) {//这个同步代码块使用的锁是object,而下面的同步函数使用的是锁是this,所以,这么干就线程不安全。
synchronized (this) {//换成this就变得安全啦。说明下面同步函数使用的锁是this
if (tick > 0) {
try {
Thread.sleep(10);
} catch (Exception ignore) {
}
System.out.println(Thread.currentThread().getName() + "....sale...代码块 : " + tick--);
}
}
}
} else {
while (true) {
show();
}
}
}
private synchronized void show() {//使用的锁就是this
if (tick > 0) {
try {
Thread.sleep(10);
} catch (Exception ignore) {
}
System.out.println(Thread.currentThread().getName() + "....sale...函数 : " + tick--);
}
}
}
可以看到上面,因为第一个继承的例子中,使用了同步代码块,或者同步函数,来解决同步问题。那么在下面这个例子中,直接把2个给弄到一起,看效果。
线程1走同步代码块,线程2走同步方法。都可以实现多线程同步的效果。
然后就是看main的代码
package com.lxk.threadTest.ticket.implement;
/**
* 卖票例子(实现Runnable接口,实现多线程)
*
* Created by lxk on 2017/6/25
*/
public class Main {
public static void main(String[] args) {
//test();
testStatic();
}
/**
* 测试普通的同步代码块和同步函数的同步效果。
* 结论:同步函数和同步代码块使用的锁都是this
*/
private static void test() {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
//现在修改为只有2个线程,1使用同步代码块,2使用同步函数。
//测试发现:两者使用的锁是不同的,因为使用的不是同一个锁,所以,线程还是不安全的。(下面分析为什么这要sleep())
t1.start();
//这时候,在没有添加下面的sleep的时候,代码一运行,所有执行结果是:全走的是同步函数,
//因为线程1启动完之后,瞬间,主线程已经把flag置成false啦,所以,2个线程都走的是false结果。
//所以,要在这1线程启动完之后,主线程休息一下,就剩1线程在跑,才能看到2个线程分别的效果。
try {
Thread.sleep(10);
} catch (Exception ignore) {
}
t.flag = false;
t2.start();
//运行结果:打印出0的错票。不安全。(此时,同步代码块使用的锁,是自己new的一个obj)
//对错误代码进行分析如下:
//两个前提。1,两个或以上的线程;2,用的是否是同一个锁。
// 后面修改同步代码块中的同步对象由object变成this,然后就安全啦。
//
//这个修改完之后,就可以看到,没有输出0啦,而且2个线程,确实都执行了不同的同步实现。一个同步代码块,一个同步函数。
//Thread t3 = new Thread(t);
//Thread t4 = new Thread(t);
//t3.start();
//t4.start();
}
private static void testStatic() {
TicketStatic t = new TicketStatic();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
//这时候,没有添加下面的sleep的时候,代码一运行,所有执行结果全走的是同步函数,
//因为线程1启动完之后,瞬间,主线程已经把flag置成false啦,所以,都走的是false结果。
//所以,要在这1线程启动完之后,主线程休息一下才能看到2个线程分别的效果。
try {
Thread.sleep(10);
} catch (Exception ignore) {
}
t.flag = false;
t2.start();
//运行结果:打印出0的错票。不安全。(这个时候,同步代码块使用的锁,是自己this)
//静态方法使用的锁和同步代码块使用的锁不一样。静态同步函数使用的锁是类.class
//对错误代码进行分析如下:
//两个前提。1,两个或以上的线程;2,用的是否是同一个锁。
// 后面修改同步函数中的同步对象由this变成.class,然后就安全啦。
//
//这个修改完之后,就可以看到,没有输出0啦,而且2个线程,确实都执行了不同的同步实现。一个同步代码块,一个同步函数。
}
}
那么再看看这另一个票的代码实现。
package com.lxk.threadTest.ticket.implement;
/**
* 测试:静态同步函数和非静态的差别
*
* Created by lxk on 2017/6/25
*/
public class TicketStatic implements Runnable {
private static int tick = 100;
boolean flag = true;
public void run() {
if (flag) {
while (true) {
//synchronized (this) {//静态同步函数使用的是类对象。
synchronized (TicketStatic.class) {//内存中没有本类对象,但是一定有该类对应的字节码文件对象。类名.class
if (tick > 0) {
try {
Thread.sleep(10);
} catch (Exception ignore) {
}
System.out.println(Thread.currentThread().getName() + "....sale...代码块 : " + tick--);
}
}
}
} else {
while (true) {
show();
}
}
}
private static synchronized void show() {//静态方法的时候,使用的锁就不是this,经测试,可发现使用的是类.class
if (tick > 0) {
try {
Thread.sleep(10);
} catch (Exception ignore) {
}
System.out.println(Thread.currentThread().getName() + "....sale...函数 : " + tick--);
}
}
}
经过测试,一般的普通同步函数,可以对this加锁,因为从代码可以看出来,这2个线程操作的都是同一个对象。
在上面最开始的以继承的方式实现多线程的第一个例子里面,是四个线程都各自操作各自的对象。
但是,那个票,是静态的,那么,静态的东西是属于类的,所以,虽然有四个对象,但是他们都操作的是一个共同的数据,那就得考虑线程安全问题,那就得考虑如何同步。
在这个实现runnable接口来实现多线程卖票的2个例子。
差别在这个地方。
一个是静态的,一个是非静态的,静态方法只能操作静态变量,所以,在第二个类里面,票变量被声明为静态的。第一个则不用。
而且,对于实现runnable接口的第一个票的例子,
发现2个线程,加锁的对象要相同,才能实现线程安全。
对于实现runnable接口的第二个票的例子,
发现,对于静态的方法而言,加锁的对象,就应该是类.class,这样才能线程安全。
有兴趣的小伙伴 可以把代码直接拿出来,自己测试一下,看看具体执行效果。更好的加深下理解。