多线程—Thread&Runable和synchronized
1. Thread & Runable
先不逼逼,上两个例子再说。
//栗子1
public class ThreadTest extends Thread {
@Override
public void run() {
//重写run方法
}
public static void main(String[] args){
Thread t=new ThreadTest ();
t.start();
}
//栗子2
public class RunnableTest implements Runnable{
@Override
public void run() {
//重写run方法
}
}
public static void main(String[] args) {
RunnableTest r=new RunnableTest();
Thread t=new Thread(r);
t.start();
}
例1,继承线程类Thread,创建子类ThreadTest,然后new一个子类对象,通过这个对象的start方法启动线程;
例2,实现Runnable接口创建子类,new一个子类对象,以这个子类对象为参数,new出一个Thread对象,最后调用start方法启动线程。好吧,很简单对吧。直接上干货。
2 Runable使用
使用Runnable更具灵活性,可以避免单继承的局限,并且可以实现多个线程同时处理同一个资源,同样还是看例子。
我们就以经典的卖票系统讲个故事吧。话说有小T(Thread)和小R(Runnable)两个票贩子,他们手下各有两个业务员,平常就干着倒卖活动门票的活(线程执行任务)。有一天,他们各拿到了5张某演唱会的门票,两人将售票的任务交给手下的业务员。
小T这边的业务是这样运转的(定义Thread类的子类,重写run()方法,run()方法即为卖票任务)
public class TicketThread extends Thread {
private int ticket = 5;
private String name;
public TicketThread(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 5; i++) {
if (ticket > 0) {
Log.e("T公司",name + "卖了一张票,编号为t" + (ticket--));
}
}
}
}
业务员一大早就开始卖票,很快就卖完了(实例化Thread的子类,调用start()方法启动该线程)
TicketThread t1 = new TicketThread("1号业务员");
TicketThread t2 = new TicketThread("2号业务员");
t1.start();
t2.start();
两个业务员卖票结果如下:
T公司:1号业务员卖了一张票,编号为t5
T公司:2号业务员卖了一张票,编号为t5
T公司:1号业务员卖了一张票,编号为t4
T公司:1号业务员卖了一张票,编号为t3
T公司:2号业务员卖了一张票,编号为t4
T公司:1号业务员卖了一张票,编号为t2
T公司:2号业务员卖了一张票,编号为t3
T公司:1号业务员卖了一张票,编号为t1
T公司:2号业务员卖了一张票,编号为t2
T公司:2号业务员卖了一张票,编号为t1
而小R的业务流程则是这样的(实现Runnable接口,重写Runnable的run()方法)
public class TicketRunnable implements Runnable {
private int ticket = 5;
public void run() {
for (int i = 0; i < 5; i++) {
if (ticket > 0) {
Log.e("R公司",Thread.currentThread().getName() + "卖了一张票,编号为r" + (ticket--));
}
}
}
}
业务员同样很快就把票卖完了(创建Thread子类的实例,将实现了Runnable接口的对象作为参数实例化Thread对象,调用start()方法启动线程)
TicketRunnable runnable = new TicketRunnable();
Thread r1 = new Thread(runnable, "1号业务员");
Thread r2 = new Thread(runnable, "2号业务员");
r1.start();
r2.start();
两个业务员卖票结果如下:
R公司:1号业务员卖了一张票,编号为t5
R公司:2号业务员卖了一张票,编号为t4
R公司:1号业务员卖了一张票,编号为t3
R公司:2号业务员卖了一张票,编号为t2
R公司:1号业务员卖了一张票,编号为t1
事毕,两人决定吃个饭庆祝一下。一番交流后,小R很惊讶小T为啥可以将同样的票多卖一倍的钱,小T得意地分享了自己的卖票心得:
“因为我将票复印了一份,这样我两个小弟就可以各拿一份票去兜售了(通过继承Thread的方式,当新创建一个线程启动时,其绑定的任务同样也新建了一份,这样他们的任务是相互独立的,自然也无法实现资源共享了)”
小R听后感叹不已:
“竟然还有这种操作?我耿直地将票交给两个业务员就算了啊(实现Runnable接口的方式,因为都是用同一个Runnable对象创建的线程,因此多线程实际上执行的是同一个任务,这样也就共享了资源)”
接着小R又问小T:“你这么干,事后不怕买到假票的人来找你?” 小T听后不以为然道:
“怕啥,我们不都是干完一票就跑路的么(线程执行完run()方法后会自行销毁)”
小R:“我从未见过有如此厚颜无耻之人......”
3.1 synchronized使用解析-同步方法(非静态)
小R(Runnable)因为诚信经营,生意越来越好了,于是小R便多招了一个业务员。某日,在售出10张门票后不久,小R就收到了顾客的投诉,说他们买到了假票。小R怀疑是自己的手下动了手脚,便展开了调查:
当时的业务流程如下
private class SellTask {
private int ticket = 10;
public void sellTicket(){
if (ticket > 0) {
try{
Thread.sleep(500);
Log.e("R公司",Thread.currentThread().getName() + "卖了一张票,编号为r" + (ticket--));
}catch (Exception e){
e.printStackTrace();
}
}
}
}
public class TicketRunnable implements Runnable {
SellTask sellTask;
public TicketRunnable(SellTask sellTask){
this.sellTask = sellTask;
}
public void run() {
for (int i = 0; i < 10; i++) {
sellTask.sellTicket();
}
}
}
票交由3个业务员去卖
SellTask sellTask = new SellTask();
TicketRunnable runnable = new TicketRunnable(sellTask);
Thread r1 = new Thread(runnable, "1号业务员");
Thread r2 = new Thread(runnable, "2号业务员");
Thread r3 = new Thread(runnable, "3号业务员");
r1.start();
r2.start();
r3.start();
调查发现,出现了多个业务员售出编号相同的票的情况
R公司:1号业务员卖了一张票,编号为r10
R公司:3号业务员卖了一张票,编号为r9
R公司:2号业务员卖了一张票,编号为r8
R公司:1号业务员卖了一张票,编号为r7
R公司:2号业务员卖了一张票,编号为r7
...
进一步调查后得知,出现这样的情况是因为业务员答应了卖给顾客某编号的票,并收取了订金,回头拿票时才发现哥几个卖得是同一张票(多个线程先后操作共享数据造成数据错误),没办法只能自己复制一张给顾客企图蒙混过关。小R不知道该怎么约束自己的手下,遂公开招聘能解决问题的人。
这天,一位自称synchronized的男人前来应聘。小R问道:“s先生有何高见啊?”s先生淡定地喝了口茶,答道:
“你现在的业务流程不太可靠(线程不安全),让我来统一管理整个售票业务,每一张票的出售都需经过我的审批,一张票卖完后业务员才能来我这再次申请拿票出售,这样每张票都只能由一个业务员进行出售,问题也自然解决了(在Java中每一个对象都有一个内部锁,当使用synchronized关键字声明某个方法时,该方法将受到对象锁的保护,这样一次就只能有一个线程可以进入该方法并获得该对象的锁,其他线程要想调用该方法,只能排队等待。当获得锁的线程执行完该方法并释放对象锁后,别的线程才可拿到锁进入该方法)。”
小R听后觉得这方法不错,便让s先生来试试。这次依然是要出售10张票,业务流程经过s先生改进后如下
private class SellTask {
private int ticket = 10;
public synchronized void sellTicket(){//使用synchronized声明sellTicket方法
if (ticket > 0) {
try{
Thread.sleep(500);
Log.e("R公司",Thread.currentThread().getName() + "卖了一张票,编号为r" + (ticket--));
}catch (Exception e){
e.printStackTrace();
}
}
}
}
与之前的SellTask类相比,该类中使用了synchronized声明sellTicket()方法。如此一来,问题果然解决了,小R悬着的心也终于放了下来。
3.2 同一个对象内使用多个同步方法
某日,小R又开始向s先生抱怨起来:“我那帮二愣子手下啊,每次进我办公室汇报工作都是乱糟糟的,让他们按顺序一个个来就是不听,s先生觉得该如何管管他们啊?”s先生依然淡定地抿了口茶,说道:“不急,容我先看看他们是怎么汇报的。”小R便依着s先生的意思安排了两个手下过来汇报工作。
private class ReportTask {
public void report1(){
Log.e("R公司","1号业务员" + "进办公室");
try{
Log.e("R公司","1号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","1号业务员" + "汇报完毕");
Log.e("R公司","1号业务员" + "出办公室");
}
public void report2(){
Log.e("R公司","2号业务员" + "进办公室");
try{
Log.e("R公司","2号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","2号业务员" + "汇报完毕");
Log.e("R公司","2号业务员" + "出办公室");
}
}
public class ReportRunnable1 implements Runnable {
ReportTask task;
public ReportRunnable1(ReportTask task){
this.task = task;
}
public void run() {
task.report1();
}
}
public class ReportRunnable2 implements Runnable {
ReportTask task;
public ReportRunnable2(ReportTask task){
this.task = task;
}
public void run() {
task.report2();
}
}
不一会儿,两个手下前后脚进了办公室,开始汇报
ReportTask reportTask = new ReportTask();
ReportRunnable1 runnable1 = new ReportRunnable1(reportTask);
ReportRunnable2 runnable2 = new ReportRunnable2(reportTask);
Thread r1 = new Thread(runnable1);
Thread r2 = new Thread(runnable2);
r1.start();
r2.start();
这俩二愣子手下是这样汇报工作的
R公司:1号业务员进办公室
R公司:1号业务员开始汇报
R公司:2号业务员进办公室
R公司:2号业务员开始汇报
R公司:1号业务员汇报完毕
R公司:1号业务员出办公室
R公司:2号业务员汇报完毕
R公司:2号业务员出办公室
小R揉了揉脑袋,叹气道:“唉,他们就是这样汇报的,每次他们一起讲的时候我都不知该听谁的。”s先生哈哈一笑,道:
“这个不难解决,下次他们再来汇报,进来第一个人我就把门锁了,让下一个在门外等,等第一个讲完了我再放第二个进来就行了(当一个线程访问对象的某个synchronized同步方法时,其他线程对对象中所有其它synchronized同步方法的访问将被阻塞)”
小R听后深以为然,便又安排刚刚那两个业务员过来重新汇报一次,这次由s先生亲自守门
private class ReportTask {
public synchronized void report1(){
Log.e("R公司","1号业务员" + "进办公室");
try{
Log.e("R公司","1号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","1号业务员" + "汇报完毕");
Log.e("R公司","1号业务员" + "出办公室");
}
public synchronized void report2(){
Log.e("R公司","2号业务员" + "进办公室");
try{
Log.e("R公司","2号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","2号业务员" + "汇报完毕");
Log.e("R公司","2号业务员" + "出办公室");
}
}
不一会儿,两个业务员又来了,这次的结果令小R非常满意
R公司:1号业务员进办公室
R公司:1号业务员开始汇报
R公司:1号业务员汇报完毕
R公司:1号业务员出办公室
R公司:2号业务员进办公室
R公司:2号业务员开始汇报
R公司:2号业务员汇报完毕
R公司:2号业务员出办公室
看着小R这么开心,s先生不禁泼起了冷水:
“你别高兴得太早,你窗户可没锁呢,说不定你那帮二愣子手下进不了门就从窗户爬进来了(当一个线程访问对象的某个synchronized同步方法时,另一个线程仍然可以访问该对象中的非synchronized同步方法)。”
果然,之后的某次工作汇报中,这样的事就发生了
private class ReportTask {
public synchronized void report1(){
Log.e("R公司","1号业务员" + "进办公室");
try{
Log.e("R公司","1号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","1号业务员" + "汇报完毕");
Log.e("R公司","1号业务员" + "出办公室");
}
public synchronized void report2(){
Log.e("R公司","2号业务员" + "进办公室");
try{
Log.e("R公司","2号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","2号业务员" + "汇报完毕");
Log.e("R公司","2号业务员" + "出办公室");
}
public void report3(){
Log.e("R公司","3号业务员" + "进办公室");
try{
Log.e("R公司","3号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","3号业务员" + "汇报完毕");
Log.e("R公司","3号业务员" + "出办公室");
}
}
//线程启动代码略...
report3()是没有修饰的,当一个线程访问对象的某个synchronized同步方法时,另一个线程仍然可以访问该对象中的非synchronized修饰的方法report3()。
3.3 synchronized同步代码块
是日,好友小T前来拜访小R,却看见小R的办公室门窗紧闭,几个业务员在门外排着队。小T十分疑惑,遂敲门招呼小R让他开门,然而却没有得到任何回应。没办法,小T只能跟着业务员在办公室外面等了
private class ReportTask {
public void report1(){
synchronized(this){
Log.e("R公司","1号业务员" + "进办公室");
try{
Log.e("R公司","1号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","1号业务员" + "汇报完毕");
Log.e("R公司","1号业务员" + "出办公室");
}
}
public void report2(){
synchronized(this){
Log.e("R公司","2号业务员" + "进办公室");
try{
Log.e("R公司","2号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","2号业务员" + "汇报完毕");
Log.e("R公司","2号业务员" + "出办公室");
}
}
public void report3(){
synchronized(this){
Log.e("R公司","3号业务员" + "进办公室");
try{
Log.e("R公司","3号业务员" + "开始汇报");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","3号业务员" + "汇报完毕");
Log.e("R公司","3号业务员" + "出办公室");
}
}
public void report4(){
synchronized (this){
Log.e("R公司","小T" + "进办公室");
}
}
}
//省略线程启动部分代码
好不容易等到门开了,一个业务员走了出来,小T便一闪身溜了进去,门立刻被小R锁上了。“你干啥呢,差点就夹到我了!”小T抱怨道。小R不好意思笑笑,说道:“原来是小T啊,你坐你坐,待会再向你解释,我先放下个业务员进来...”
R公司:1号业务员进办公室
R公司:1号业务员开始汇报
R公司:1号业务员汇报完毕
R公司:1号业务员出办公室
R公司:2号业务员进办公室
R公司:2号业务员开始汇报
R公司:2号业务员汇报完毕
R公司:2号业务员出办公室
R公司:小T进办公室
R公司:3号业务员进办公室
R公司:3号业务员开始汇报
R公司:3号业务员汇报完毕
R公司:3号业务员出办公室
好不容易应付完所有业务员,小R向小T解释了来龙去脉,并拿出了一把钥匙交给小T
private class ReportTask {
public void report1(){
synchronized(this){
//省略部分代码...
}
}
public void report2(){
synchronized(this){
//省略部分代码...
}
}
public void report3(){
synchronized(this){
//省略部分代码...
}
}
private String window = "window";
public void report4(){
synchronized (window){
Log.e("R公司","小T" + "进办公室");
}
}
}
"s先生自然有应对这种情况的妙计,这把钥匙可以打开窗户(持有window对象的内置锁),你以后可以从窗户直接爬进来,不用在门外排队(synchronized (obj){}同步代码块和用synchronized声明方法的作用基本一致,都是对synchronized作用范围内的代码进行加锁保护,其区别在于synchronized同步代码块使用更加灵活、轻巧,synchronized (obj){}括号内的对象参数即为该代码块持有锁的对象。例如上述例子中,前面三个report方法中的同步代码块持有锁的对象为ReportTask的实例对象,而report4方法中的同步代码块持有锁的对象则为window。因为对象都有自己的对象锁,只能保护属于自己的同步代码块或同步方法,所以即使其他线程进入前三个方法的同步代码块中并获得相应对象的锁,也不会阻塞进入report4方法的线程执行其中的同步代码)。"
拿到钥匙后,小T再也不用和业务员一起在门外排队了
3.4 区分静态同步方法和非静态同步方法
了解了上述知识后,我们回过头再来理解同步方法和静态同步方法的区别。从持有锁的对象的不同我们可以将synchronized同步代码的方式分为两大派系:
- synchronized声明非静态方法、同步代码块的synchronized (this){}和synchronized (非this对象){}这三者持有锁的对象为实例对象(类的实例对象可以有很多个),线程想要执行该synchronized作用范围内的同步代码,需获得对象锁。
public class SynchronizedTest {
public synchronized void test1(){
//持有锁的对象为SynchronizedTest的实例对象
}
public void test2(){
synchronized (this){
//持有锁的对象为SynchronizedTest的实例对象
}
}
private String obj = "obj";
public void test3(){
synchronized (obj){
//持有锁的对象为obj
}
}
}
- synchronized声明静态方法以及同步代码块的synchronized (类.class){}这两者持有锁的对象为Class对象(每个类只有一个Class对象,而Class对象是Java类编译后生成的.class文件,它包含了与类有关的信息),线程想要执行该synchronized作用范围内的同步代码,需获得类锁
public class SynchronizedTest {
public static synchronized void test4(){
//持有锁的对象为SynchronizedTest的Class对象(SynchronizedTest.class)
}
public void test5(){
synchronized (SynchronizedTest.class){
//持有锁的对象为SynchronizedTest的Class对象(SynchronizedTest.class)
}
}
}
有关实例对象和Class对象的详细知识大家可以继续在网上查找资料进行深挖,这里就不赘述了,总之我们要记住的一点是
若synchronized同步方法(代码块)持有锁的对象不同,则多线程执行相应的同步代码时互不干扰;若相同,则获得该对象锁的线程先执行同步代码,其他访问同步代码的线程会被阻塞并等待锁的释放