线程之间的关系
- 线程间同步:相互竞争 相互排斥
- 线程间协作:线程间相互协作
线程同步
问题引出
一个多线程的程序如果是通过Runnable接口实现的,则意味着类中的属性将被多个线程共享,由此引出资源的同步问题,即当多个线程要操作同一资源时,有可能出现错误。
练习: 分别使用两种编程方法实现买票的程序
1. 继承Thread方法
public class TestTicketThread{
public static void main(String[] a){
TicketThread tThread1 = new TicketThread();
TicketThread tThread2 = new TicketThread();
TicketThread tThread3 = new TicketThread();
tThread1.start();
tThread2.start();
tThread3.start();
}
}
class TicketThread extends Thread {
private int ticket = 5;
public void run(){
for (int i = 0; i < 5; i++){
if (ticket > 0){
System.out.println(this.getName() + "卖票:ticket = " + ticket--);
}
}
}
}
运行结果:每个线程单独买票
- 实现Runnable接口
package zhi;
/**
* Created by admin on 2017/5/17.
*/
public class TestTicketRunnable{
public static void main(String[] a){
TicketRunnable Thread = new TicketRunnable();
new Thread(Thread).start();
new Thread(Thread).start();
new Thread(Thread).start();
}
}
class TicketRunnable implements Runnable {
private int ticket = 5;
@Override
public void run() {
for (int i = 0; i < 5; i++){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖票:ticket = " + ticket--);
}
}
}
}
运行结果:
Thread-0卖票:ticket = 5
Thread-0卖票:ticket = 2
Thread-2卖票:ticket = 3
Thread-1卖票:ticket = 4
Thread-0卖票:ticket = 1
- 结果分析
第二种方法,虽然启动了3个线程,但是3个线程一共卖了5张票,即ticket属性被所有的线程对象共享。而第一种方法,3个线程各卖了5张票,没有实现属性的共享。
结论:实现Runnable接口的方法相对于继承Thread类来说,适合多个相同程序代码的线程去处理统一资源的情况。
练习: 在上述卖票事例的基础上,做如下修改
public class TestTicketRunnable{
public static void main(String[] a){
TicketThread tThread = new TicketThread();
new Thread(tThread).start();
new Thread(tThread).start();
new Thread(tThread).start();
}
}
class TicketThread implements Runnable {
private int ticket = 5;
public void run(){
for (int i = 0; i < 5; i++){
if (ticket > 0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:ticket = " + ticket--);
}
}
}
}
运行结果:
Thread-0卖票:ticket = 5
Thread-2卖票:ticket = 5
Thread-1卖票:ticket = 4
Thread-1卖票:ticket = 2
Thread-2卖票:ticket = 3
Thread-0卖票:ticket = 3
Thread-0卖票:ticket = -1
Thread-1卖票:ticket = 1
Thread-2卖票:ticket = 0
分析:卖票的业务步骤如下:
1. 判断票数是否大于0,大于0则表示还有票可以卖;
2. 如果票数大于0,则将票卖出。
在上面的代码中,在步骤(1)和(2)之间加入了延迟操作,那么一个线程就有可能在判断完之后,还没有对票数进行减操作之前,其他线程就已经将票数减少了,这样一来就会出现票数为负的情况。
要解决这个问题,必须使用同步。同步是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成之后才可以继续执行。
使用synchronized同步代码块——隐式加锁
为避免竞争状态,应该防止多个线程同时进入程序的某一个特定的部分,程序中的这部分称为临界区。
第一种方法可以在代码块上加上synchronized关键字,则此代码块就称为同步代码块。
同步代码块同一时刻只能有一个线程访问
练习: 修改上面的代码,用同步机制解决资源共享问题
public class TestTicketRunnable{
public static void main(String[] a){
TicketThread tThread = new TicketThread();
new Thread(tThread).start();
new Thread(tThread).start();
new Thread(tThread).start();
}
}
class TicketThread implements Runnable {
private int ticket = 5;
public void run(){
for (int i = 0; i < 5; i++){
synchronized(this){
if (ticket > 0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:ticket = " + ticket--);
}
}
}
}
}
运行结果:
Thread-0卖票:ticket = 5
Thread-0卖票:ticket = 4
Thread-2卖票:ticket = 3
Thread-2卖票:ticket = 2
Thread-2卖票:ticket = 1
分析:使用同步代码块之后,没有出现重复票和票为负数的情况。
在方法前加入synchronized关键字,则该方法为同步方法。
public class TestSynchronizedMethod {
public static void main(String[] a){
TicketThreadMethod tThread = new TicketThreadMethod();
new Thread(tThread).start();
new Thread(tThread).start();
new Thread(tThread).start();
}
}
class TicketThreadMethod implements Runnable {
private int ticket = 5;
public void run(){
for (int i = 0; i < 5; i++){
this.sale();
}
}
public synchronized void sale(){
if (ticket > 0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:ticket = " + ticket--);
}
}
}
利用加锁同步——显式加锁lock.lock()
线程协作在研究