上篇的代码,存在一个问题,那就是多线程有可能操作到同一个资源块,解决这个问题的方式就是同步代码快或者加上锁。锁的作用就是让资源的占用总是被一个线程调用,而不会使多个线程发生抢占。
那么如何解决线程安全问题呢,第一是同步代码快:
package com.qf.duoxian;
public class Ticket1 implements Runnable {
public Ticket1() {
// TODO Auto-generated constructor stub
}
int ticket = 100;
Object
lock = new Object();
@Override
public void run() {
while (true){
synchronized (lock) {
if(ticket>0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"谁买了票"+ticket--);
}
}
}
}
}
class ThreadDemo2{
public static void main(String[] args) {
Ticket1 ticket1 = new Ticket1();
Thread t1 = new Thread(ticket1,"卖给我火车票a");
Thread t2 = new Thread(ticket1,"卖给我火车票b");
Thread t3 = new Thread(ticket1,"卖给我火车票c");
t1.start();
t2.start();
t3.start();
}
}
这是打印印出来的结果:
卖给我火车票a谁买了票100
卖给我火车票a谁买了票99
卖给我火车票a谁买了票98
卖给我火车票a谁买了票97
还有就是同步方法。
第二种就是上锁:
Lock
实现提供了比使用 synchronized
方法和语句可获得的更广泛的锁定操作。
void lock()
获取锁。
void unlock()
释放锁。
实例:更简单。
public class Ticket2 implements Runnable {
public Ticket2() {
// TODO Auto-generated constructor stub
}
int ticket = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
if(ticket>0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买票了"+ticket--);
lock.unlock();
}
}
}
}
class ThreadPool{
public static void main(String[] args) {
Ticket2 ticket = new Ticket2();
Thread 他 = new Thread(ticket,"窗口1");
Thread 他1 = new Thread(ticket,"窗口2");
Thread 他2 = new Thread(ticket,"窗口3");
他.start();
他1.start();
他2.start();
}
}
死锁:
同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
等待唤醒机制:
在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即——等待唤醒机制。
比如:实现一功能,input为一线程,output为一线程,main为主线程,实现input对成员变量赋值,output获取成员变量值。
但有一个问题是当input给一个成员变量赋值后,output开始获取成员变量值,此时还没有获取完全,紧接着input给下一个成员变量赋值,这就导致了output获取的值可能是input第二次给成员变量赋的值,要想解决此办法,必须让input和output这两个线程实现通信,保证一边input赋值后,等待output获取值,当其获取值后,给input一个信号,使其再赋值。才能保证不会出现乱值。
等待唤醒机制所涉及到的方法:
wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
1.当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
2.当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。
public class Resources {
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name, String sex) {
if(flag)
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out(){
if(!flag)
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("姓名是:"+name+"年龄:"+sex);
flag = false;
this.notify();
}
}
public class Output implements Runnable {
private Resources r;
public Output(Resources r) {
this.r = r;
}
@Override
public void run() {
while (true){
r.out();
}
}
}
class ThreadDemo3{
public static void main(String[] args) {
Resources r = new Resources();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
public class Input implements Runnable {
private Resources r;
public Resources getR() {
return r;
}
public Input(Resources r) {
this.r = r;
}
public void setR(Resources r) {
this.r = r;
}
@Override
public void run() {
int count = 0;
while(true){
if(count ==0){
r.set("小明", "男");
}else{
r.set("小花", "女");
}
count = (count+1)%2;
}
}
}
线程池
线程池概念:
线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
我们详细的解释一下为什么要使用线程池?
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
使用线程池方式:
1. Runnable接口
2. Callable接口