2.实现Runnable 接口
步骤:
1、定义类实现Runnable接口
2、覆盖Runnable接口中的run方法,运行的代码放入run方法中。
3、通过Thread类建立线程对象。
4、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定指定对象的run方法。就必须明确该run方法所属对象
5、调用Thread类的start方法开启线程并调用Runnable接口子类的run方法
代码示例:卖票程序,多个窗口同时卖票
class Ticket implements Runnable{
private int tick = 100;
public void run(){
while(true){
if(tick>0) {
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);//创建了一个线程;
Thread t2 = new Thread(t);//创建了一个线程;
Thread t3 = new Thread(t);//创建了一个线程;
Thread t4 = new Thread(t);//创建了一个线程;
t1.start();
t2.start();
t3.start();
t4.start();
}
}
这里使用线程类创建4个实例对象(窗口),共同卖100张票
注:这里需要注意线程同步问题
什么时候会出现线程安全问题?
1.在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源
2.当多个线程同时访问同一个资源的时候,就会存在一个问题:由于每个线程执行的过程是不可控的,所以很可能导致最终的结果
与实际上的愿望相违背或者直接导致程序出错
举个简单的例子:以卖票举例
现在有两个线程分别从窗口卖票,要求不能卖出重复的票。
那么必然在卖票的过程中存在两个操作:
(1)检查该票是否已经卖出;
(2)如果卖出,则不卖;如果没有卖出,则卖出该票。
假如两个线程分别用thread-1和thread-2表示,某一时刻,thread-1和thread-2都卖出票A,那么可能会发生这种情况;
(1)thread-1去检查票A是否卖出,然后thread-2也接着去检查票A是否卖出;
(2)结果两个线程检查的结果都是票A没有卖出,那么两个线程都分别将票A卖出。
这个就是线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果;
这里面,这个资源被称为:临界资源(也有称为共享资源)。
也就是说,当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程安全问题。
注:当多个线程执行一个方法,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题
解决多线程安全性问题:
通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
1、同步代码块
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//定义锁对象
Object lock = new Object();
@Override
public void run() {
//模拟卖票
while(true){
//同步代码块
synchronized (lock){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
}
2、同步函数:锁为this(推荐使用)
class Ticket implements Runnable{
private int tick = 100;
public void run(){
while(true){
//这里面的this就是一把锁,使用这个类创建的线程使用同一把锁
synchronized (this) {
if(tick>0) {
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
3、Lock对象(本身是一个接口,使用其子类,强烈推荐使用)
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//创建Lock锁对象
Lock ck = new ReentrantLock();
@Override
public void run() {
//模拟卖票
while(true){
ck.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
//释放锁
ck.unlock();
}
}
同步synchronized与锁机制Lock区别:
(1) 线程睡眠一旦产生异常,去捕获异常,同步方法不能结束,同步锁就不能被释放。因此JDK1.5后,采用Lock接口可替代synchronized
(2)使用Lock接口,以及其中的lock()方法和unlock()方法替代同步
死锁现象:
(1)当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步,这时容易引发死锁
(2)程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉
synchronzied(A锁){
synchronized(B锁){
}
}
扩展:
(1)sleep()和wait()方法的区别?
sleep(): 不释放锁对象, 释放CPU使用权,在休眠的时间内,不能唤醒
wait(): 释放锁对象, 释放CPU使用权,在等待的时间内,能唤醒
(2)为什么wait(),notify(),notifyAll()等方法都定义在Object类中?
锁对象可以是任意类型的对象!!!
使用Runnable接口创建线程的优缺点:
1.优点:
(1) 适合多个相同的程序代码的线程去处理同一个资源
(2)可以避免java中的单继承的限制
(3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
(4)线程池有很好的支持
2.缺点:
(1)在使用Runnable定义的子类中没有start()方法,只有Thread类中才有
(2)通过Thread类来启动Runnable实现的多线程
综上所述,Runnable接口创建线程优势远远大于劣势,所以Runnable比Thread应用更广!!