Java多线程要点分析总结
1:继承Thread类跟实现Runnable接口
实现Runnable接口的优点:摆脱单继承的局限
可以实现资源共享
来看一下Thread类的部分源码:
public class Thread implements Runnable { private Runnable target; // What will be run. public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); //Initializes a Thread. } public void run() { //覆写了run方法 if (target != null) { target.run(); } } public synchronized void start() { if (threadStatus != 0) // 如果线程已经启动,就抛出异常 throw new IllegalThreadStateException(); group.add(this); start0(); // 调用本地方法启动线程 if (stopBeforeStart) { stop0(throwableFromStop); } } private native void start0(); }API中对Runnable接口的说明:设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如,Thread 类实现了 Runnable。
多线程编程中,涉及到两个类((多线程代码封装类,Runnable接口实现类),Thread类(可以启动新线程)),
下面结合上面Thread类的源码来分析一下继承Thread类跟实现实现Runnable接口的小区别:
继承Thread类:我们的类会覆写Thread类的run方法,并把线程执行代码封装进run方法,然后调用Thread类的start方法启动线程。
要注意的是,Thread类也实现了Runnable接口,这就是为什么我们能把Thread类当成多线程代码封装类来使用的原因,因为它能去做这件事。
但这不是他的本职工作,顶多算扩展功能。他的本质工作是启动新线程。
实现Runnable接口:在线程对象初始化的时候通过构造方法传入Runnable接口实现类,然后Thread类的start方法会调用run方法,
run方法中判断target不等于null,于是执行的是我们Runnable接口实现类中的run方法。这也就是为什么能实现对象资源共享的原因,因为可以操作同一个对象。
有很多人说实现Runnable接口来创建线程对象,应用了代理模式。我们来看一下吧,
代理模式的定义:一个角色代表另一个角色来完成某些特定的功能
代理模式有三个角色: 1. 抽象主题角色 2. 代理主题角色 3. 实际被代理角色,如果应用了静态代理模式,那么对应的角色如下:
这里的Runnable接口作为抽象主题角色提供一个公共协议,Thread类作为主题角色实现了该协议,我们的Runnable实现类作为实际被代理角色也实现了该协议。
如果是代理模式,那么我们对被代理角色的访问应该可以是Runnable thread = new Thread(),然后thread.run方法执行被Thread类代理的方法。
但在这里,这样调用对于多线程来说是没有意义的,不会启动新的线程,仅仅就是普通方法调用,抛弃多线程,那么这里的确是静态代理模式。
这对于Thread类来说,他是通过Thread t = new Thread(); t.start(); start方法再去调用run方法。仅仅就是多态模式下的方法调用罢了。
Thread类之所以实现Runnable接口,是为了本身的功能扩展。上面已经分析过了。即使不实现Runnable接口,一样可以使用第二种方式来创建新线程对象,那这还属于代理模式吗?
2:多线程同步问题
问题的引出:通过实现Runnable接口的方式来创建新线程解决了多条线程数据共享问题。那么项目在实际运行的过程中,由于操作过程可能不仅仅是对数据进行单纯的修改操作,还可能有对数据的判断操作,临时缓存,数据库操作等,
在这些操作过程中,因为网络延时,线程切换等就会触发数据覆盖,判断异常等问题。
例A<数据覆盖问题>:
class DoSomething { private int a = 10; public void add(int b) { int temp = a; // 缓存现有值 temp += b; // 缓存累加后的值 try { Thread.sleep(100); // 模拟处理数据的时间或者线程切换。 } catch (Exception e) { } a = temp; // 模拟写入操作。 System.out.println(getA()); // 输出当前线程对a进行累加操作后的值 } public int getA() { return this.a; } } public class ThreadDemo { private static Thread[] threads = new Thread[10]; public static void main(String[] args) { final DoSomething ds = new DoSomething(); for (int i=0; i<10; i++) { // 开启10条线程进行操作 threads[i] = new Thread(new Runnable(){ @Override public void run() { ds.add(1); } }); threads[i].start(); } } }观察以上程序,发现10个线程每次写入之后,a都等于11,10个线程写了10次,应该+10等于20啊?
为什么是11呢?详细分析一下:
比如A线程进入add方法开始操作数据(将累加的数据缓存给局部变量temp),完成了数据的缓存,还没修改a的值,A线程进入休眠状态,
然后B线程进入add方法开始操作数据(将累加的数据缓存给局部变量temp),完成了数据的缓存,还没修改a的值,B线程进入休眠状态,
然后A线程重新进入运行状态,完成了数据的修改操作,a=11;B线程进入运行状态,完成了对数据的修改操作,a=11。
哪里有问题?应该出来了吧?局部变量temp的值被后来的b线程覆盖掉了。所以,即使10个线程执行add方法,每次temp都是11。
解决方式:
对add方法进行同步。修改后,public synchronized void add(int b),这样便使用同步,解决了以上问题。输出如下:
例B<判断异常问题>
class DoSomething { private int ticket = 50; public void sell() { for (int i=0; i<50; i++) { if (ticket > 0) { // 还有票 try { Thread.sleep(100); // 模拟处理数据的时间或者线程切换。 } catch (Exception e) {} System.out.println(Thread.currentThread().getName() + "卖出第" + ticket-- + "张票"); } } } } public class ThreadDemo { public static void main(String[] args) { final DoSomething ds = new DoSomething(); for (int i=0; i<3; i++) { // 开三条线程开始卖票 new Thread(new Runnable(){ @Override public void run() { ds.sell(); } }).start(); } } }以上程序最后输出了-1;在哪出问题了呢?在判断ticket>0的时候,比如现在ticket=1,A线程进入判断大于0,然后休眠了。
B线程进入判断大于0,执行了ticket--,ticket成0了,A线程回到运行状态,执行了ticket--,ticket成-1了。
解决方法,对sell方法进行同步,或者对if判断使用同步代码块进行同步。
我们实现线程安全的方式是互斥同步,即使用原语synchronized进行同步。
Synchronized关键字:
编译后会在同步块前后分别形成monitorenter和monitorexit这两个字节码指令。
这两个指令都需要一个引用类型的参数来指明要锁定和解锁的对象。如果没有明确指定对象参数,
那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。
在执行monitorenter指令时,首先尝试获取对象的锁,如果没有被锁定或者当前线程已经拥有了该对象的锁,则将锁计数器加1,
相应的执行moniterexit时,将锁计数器减1,当计数器为0时,锁就被释放了。如果获取对象锁失败,则当前线程就要阻塞等待。
3:多线程死锁问题
死锁就是两个或两个以上的线程被无限的阻塞,线程之间相互等待所需资源。可能发生在以下情况:
当两个线程相互调用Thread.join();
当两个线程使用嵌套的同步块,一个线程占用了另外一个线程必须的锁,互相等待时被阻塞就有可能出现死锁。
写一个死锁出来:
class MyLock { static final Object lockA = new Object(); static final Object lockB = new Object(); } class DoSomething implements Runnable { private boolean flag; public DoSomething(boolean flag) { this.flag = flag; } @Override public void run(){ if (flag) { synchronized(MyLock.lockA) { System.out.println("锁住A,我要进B"); synchronized(MyLock.lockB) { System.out.println("MyLock.lockB"); } } } else { synchronized(MyLock.lockB) { System.out.println("锁住B,我要进A"); synchronized(MyLock.lockA) { System.out.println("MyLock.lockA"); } } } } } public class ThreadDemo2 { public static void main(String[] args) { new Thread(new DoSomething(true)).start(); new Thread(new DoSomething(false)).start(); } }
4:生产者消费者
生产者不断生产,消费者不断消费。class Person { private String name; private char sex; private boolean flag; public void setName(String name) { this.name = name; } public String getName() { return this.name; } public void setSex(char sex) { this.sex = sex; } public char getSex() { return this.sex; } public void setFlag(boolean flag) { this.flag = flag; } public boolean getFlag() { return this.flag; } } /* 生产者 */ class Producer implements Runnable { private Person per; public Producer(Person per) { this.per = per; } public void run() { // 生产者不断生产内容 int i = 0; while(true) { synchronized(per) { if(per.getFlag()) { // 如果为真就等待 try { per.wait(); // 等待线程 } catch (Exception e) { } } else { if(i == 0) { per.setName("西门庆"); per.setSex('男'); } else { per.setName("潘金莲"); per.setSex('女'); } System.out.println(Thread.currentThread().getName() + "号线程开始生产:" + per.getName() + " - " + per.getSex()); i = (i+1)%2; per.setFlag(true); per.notify(); // 唤醒等待线程 } } } } } /* 消费者 */ class Consumer implements Runnable { private Person per; public Consumer(Person per) { this.per = per; } public void run() { // 消费者不断取走内容 while(true) { synchronized(per) { if(!per.getFlag()) { // 如果为假 try { per.wait(); // 等待线程 } catch (Exception e) { } } else { System.out.println("\t" + Thread.currentThread().getName() + "号线程开始消费:" + per.getName() + " - " + per.getSex()); per.setFlag(false); per.notify(); } } } } } /** * 生产者与消费者 线程通信问题 应用等待唤醒机制 */ public class ThreadCommunication { public static void main(String[] args) { Person per = new Person(); Producer prod = new Producer(per); Consumer cons = new Consumer(per); new Thread(prod).start(); // 启动线程开始生产 new Thread(cons).start(); // 启动线程开始消费 } }
5:模拟银行存取案例
业务分析:模拟银行中多个线程同时对一个储蓄账户进行存款,取款操作。
Account:个人账户
Bank:取款,存款,查询余额
class Account { private String name; private double amount; public Account(String name, double amount) { this.name = name; this.amount = amount; } public void setName(String name) { this.name = name; } public String getName() { return this.name; } public void setAmount(double amount) { this.amount = amount; } public double getAmount() { return this.amount; } } class Bank { private Account account; public Bank(Account account) { this.account = account; } // 存钱 public synchronized void deposit(double amount) { double tmp = account.getAmount(); // 缓存现有金额 tmp += amount; // 缓存累加之后的金额 try { Thread.sleep(100); // 模拟处理需要的时间 } catch (Exception e) { } account.setAmount(tmp); // 写入操作 System.out.println(getBalance()); } // 取钱 public synchronized void withdraw(double amount) { double tmp = account.getAmount(); // 缓存现有金额 tmp -= amount; try { Thread.sleep(100); // 模拟处理需要的时间 } catch (Exception e) { } account.setAmount(tmp); // 写入操作 } // 查询余额 public String getBalance() { return account.getName() + "---" + account.getAmount(); } } public class ThreadDemo { private static int NUM_OF_THREAD = 10; static Thread[] threads = new Thread[NUM_OF_THREAD]; public static void main(String[] args) { final Bank bank = new Bank(new Account("张三",3000.00d)); for (int i=0; i<NUM_OF_THREAD; i++) { threads[i] = new Thread(new Runnable(){ @Override public void run() { bank.deposit(100.00d); bank.withdraw(100.00d); } }); threads[i].start(); } for (int i=0; i<NUM_OF_THREAD; i++) { try { threads[i].join(); } catch (Exception e) {} } System.out.println("余额:" + bank.getBalance()); } }
其它常用方法:
object.wait()wait()方法用在同步的代码块里,当wait()被执行时,锁会被释放,当前线程进入等待队列。
只有该对象的锁被释放,并不会释放当前线程持有的其他同步资源。
如果当前线程在等待之前或在等待时被任何线程中断,则会抛出InterruptedException。
object.notify()
方法notify()同样用在同步的代码块里。唤醒在此对象锁上等待的单个线程。此方法只能由拥有该对象锁的线程来调用。
notify()将通知等待相同锁的线程,如果有多个线程在等待这个锁,将从中随机选择一个进行通知。
从锁的等待队列里面随机唤醒某一个线程,唤醒之后的线程进入就绪队列,等待被选择执行。
Thread.sleep()
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
监控状态依然保持、会自动恢复到可运行状态,不会释放对象锁。如果任何线程中断了当前线程。
当抛出InterruptedException异常时,当前线程的中断状态被清除。让出CPU分配的执行时间。
不推荐在同步方法中调用sleep方法,原因你懂的,不懂看上面例子就知道了吧,那叫没事找事。
thread.join():在一个线程对象上调用,使当前线程等待这个线程对象对应的线程结束。
Thread.yield():暂停当前正在执行的线程对象,并执行其他线程。
interrupt()
使当前线程的状态变为中断线程,中断一个不处于活动状态的线程不会有任何作用。
如果线程在调用Object类的wait()方法,或者join()、sleep()方法过程中受阻(被中断),则其中断状态将被清除,并收到一个InterruptedException。
Thread.interrupted():检测当前线程是否已经中断,并且清除线程的中断状态(回到非中断状态)。
isAlive():如果线程已经启动且尚未终止,则为活动状态。
setDaemon():需要在start()方法调用之前调用。当正在运行的线程都是后台线程时,Java虚拟机将退出。否则当主线程退出时,其他线程仍然会继续执行。
原创文章,转载请注明出处:http://blog.csdn.net/thinking_in_android