实现多线程
一、进程和线程
1.进程
(1)进程是正在运行的程序。
(2)特点:
1)进程是系统进行资源分配和调用的独立单位;
2)每个进程都有自己的内存空间和系统资源。
2.线程(Thread)
(1)线程是进程中的单个顺序控制流,是一条执行路径。
(2)单线程与多线程:
1)单线程:一个进程如果只有一条执行路径,则称为单线程程序;
2)多线程:一个进程如果有多条执行路径,则称为多线程程序。
二、多线程的实现方式
(一)通过继承Thread类实现多线程
1.创建一个新的执行线程的步骤:
(1)定义一个Thread的子类MyThread;
(2)在MyThread中重写run()方法;
(3)创建MyThread子类对象;
(4)启动线程。
2.为什么要重写run()方法?
重写run()方法来封装被线程执行的代码。
3.run()和start()方法的区别?
run() |
封装被线程执行的代码,直接调用run()相当于普通方法的调用。 |
start() |
启动线程,然后由JVM调用此线程的run(),这样可以实现多线程。 |
4.设置和获取线程名称
(1)Thread类中设置和获取线程名称的方法:
void setName(String name) |
将此线程的名称更改为指定名称name。 |
String getName() |
返回此线程的名称。 |
(2)通过带参构造方法设置线程名称,通过getName()获取线程名称。
(3)如何获取当前正在执行线程的名称?
Thread中有一个方法:
static Thread currentThread()该方法用来返回当前正在执行的线程对象,再使用getName()获取线程名称。
5.线程调度模型
(1)分时调度模型:所有线程轮流获得CPU使用权,平均分配每个线程占用CPU的时间片。
(2)抢占式调度模型:优先让优先级高的线程使用CPU,优先级高的线程获得的时间片相对多一些;如果优先级相同,则随机选择其中一个。
【tips】:Java中使用的是(2)抢占式调度模型。
(3)Thread中设置和获取线程优先级的方法:
void setPriority(int newPriority) |
更改次线程的优先级为指定值newPriority。 |
int getPriority() |
返回此线程的优先级。 |
②线程的优先级高仅仅代表获得CPU时间片的几率高,而不是说一定比优先级低的先执行。
6.线程控制方法
static void sleep(long millis) |
使当前正在执行的线程停止指定的毫秒数后再执行。 |
void join() |
其他线程必须等待调用jion()的线程执行完(死亡)后才能执行。 |
void setDaemon(boolean on) |
将此线程标记为守护线程,当运行的线程都为守护线程时,JVM退出。 |
7.线程生命周期
(二)通过Runnable接口的实现类实现多线程
1.创建一个新的执行线程的步骤:
(1)创建Runnable接口的实现类RunnableImpl;
(2)在RunnableImpl中重写run()方法;
(3)创建RunnableImpl对象ri;
(4)创建Thread对象,把ri作为构造方法Thread(Runnable ri)的参数;
(5)启动线程。
【tips】:Thread(Runnable target, String name)这个带参构造可以设置线程名称。
2.Runnable接口的实现类实现多线程的优点
(1)第(一)种方法由于类继承了Thread,就不能再继承别的类了;而通过Runnable接口的实现类实现多线程,可以继承别的类,避免了Java只能单继承的局限性;
(2)由于通过Runnable接口的实现类实现多线程时,把实现类对象作为Thread带参构造的参数,这样适合多个相同程序的代码来处理同一个资源的情况,把线程和程序的代码、数据分开来,体现了面向对象的设计思想。
三、多线程程序的数据安全问题
1.判断是否会有数据安全问题的标准
(1)是否是多线程环境;
(2)是否有共享数据;
(3)是否有多条语句操作共享数据。
只要同时满足以上三个条件,就可能会出现数据安全问题。
2.如何解决多线程的数据安全问题?
破坏上面的条件(3):使用同步代码块synchronized把操作共享数据的多条语句锁起来,让任意时刻只能有一个线程执行这些语句。
(1)同步代码块格式:
synchronized(任意对象){ 多条语句操作共享数据的代码; }【tips】:这里的任意对象就可以看做一把锁,而对多个线程的来说应该共用这一把锁,所以不能在synchronized的()里直接new一个Object,而应该把这个锁也当成多个线程的共享数据。
(2)加锁的优缺点:
1)优点:解决了多线程的数据安全问题;
2)缺点:因为每个线程都会去判断同步上的锁,所以会降低程序的运行效率。
3.同步方法——synchronized关键字
(1)synchronized
synchronized是java中的关键字,是一种同步锁。synchronized可以:
- 修饰一个代码块,被修饰的代码块称为同步代码块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修饰一个静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。
(2)同步方法就是把synchronized关键字加在方法上:
1)格式:
修饰符 synchronized 返回值类型 方法名(参数){}2)同步方法的锁对象:this,即调用这个方法的对象。
(2)同步静态方法就是把synchronized关键字加在静态方法上:
1)格式:
修饰符 static synchronized 返回值类型 方法名(参数){}
4.几个线程安全的类
(1)StringBuffer:线程安全,是可变的字符序列。
从版本JDK 5开始,StringBuffer已经被StringBuilder 补充了,它被设计为使用一个线程。如果不需要线程安全的实现,建议使用StringBuilder代替StringBuffer,因为它支持所有相同的操作,但它更快,因为它不执行同步。
(2)Vector:实现了可扩展的对象数组,Vector被同步,线程安全。
从Java 2平台开始,Vector类实现了List接口,成为Collections的系列成员。如果不需要线程安全的实现,建议使用ArrayList代替Vector 。
(3)Hashtable:实现了一个哈希表,它将键映射到值。 任何非null对象都可以用作键值或值。Hashtable被同步,线程安全。
从Java 2平台开始,Hashtable类实现了Map接口,成为Collections的系列成员。如果不需要线程安全的实现,建议使用HashMap代替Hashtable 。
【tips】:一般来说,在多线程实现中,Vector和Hashtable也不被使用,而用Collections中的两个方法来代替:
①static <T> List<T> synchronizedList(List<T> list):返回由指定列表list支持的同步(线程安全)列表。
②static <K,V> Map<K,V> synchronizedMap(Map<K,V> m):返回由指定映射m支持的同步(线程安全)映射。
5.Lock锁
(1)Lock接口提供了比使用synchronized方法和语句可以获得的更广泛的锁操作。
(2)锁方法:
void lock() |
加锁。 |
void unlock() |
解锁。 |
构造方法:ReentrantLock()
(4)使用方法:
public class SellTickts implements Runnable { private int tickts = 100; private Lock l = new ReentrantLock();//在Runnable的实现类里通过多态的方式定义一个共享的锁。 @Override public void run() { while (true) { try { l.lock();//加锁 if (tickts > 0) { System.out.println(Thread.currentThread().getName() + "正在卖第" + (101 - tickts) + "张票"); tickts--; } } finally { l.unlock();//解锁 } } } }【tips】:这里注意一般要使用try...finally...的方式来加锁和解锁,而不是像下面这样直接把操作共享数据的多条语句夹在加锁和解锁之间,因为一旦这些多条语句出现异常,就不会解锁了。
public class SellTickts implements Runnable { private int tickts = 100; private Lock l = new ReentrantLock(); @Override public void run() { while (true) { l.lock();//夹 if (tickts > 0) { System.out.println(Thread.currentThread().getName() + "正在卖第" + (101 - tickts) + "张票"); tickts--; } l.unlock();//夹 } } }
6.生产者消费者问题模型——线程间的等待与唤醒
Object类中提供了线程等待和唤醒的方法:
void wait() |
使当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。 |
void notify() |
唤醒正在等待的单个线程。 |
void notifyAll() |
唤醒正在等待的所有线程。 |