java多线程的学习之路(二)
一、临界资源问题
模拟四个售票员同时售票的场景
public class SourceConflict {
//演示临界资源问题
//某个景点有4个售票员在同时售票。
public static void main(String[] args) {
//1 实例化四个售票员,用4个线程模拟4个售票员
Runnable r = () -> {
while (TicketCenter.restCount > 0) {
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + --TicketCenter.restCount);
}
};
Thread t1 = new Thread(r, "售票员1");
t1.start();
Thread t2 = new Thread(r, "售票员2");
t2.start();
Thread t3 = new Thread(r, "售票员3");
t3.start();
Thread t4 = new Thread(r, "售票员4");
t4.start();
}
}
class TicketCenter {
//描述剩余的票的数量
public static int restCount = 100;
}
部分输出结果下如图
可以发现余票不是顺序减少的:这是因为一个线程强夺到CPU以后,做完减法还没来得及输出,CPU又被另一个线程强夺了
并且出现了两次剩余97
出现这些问题是由于多个线程同时访问同一个临界资源
二、同步代码段
为了解决上面的临界资源问题,可以对访问临近资源时加锁
添加同步代码段
public class SynchronizedDemo {
//演示临界资源问题
//某个景点有4个售票员在同时售票。
public static void main(String[] args) {
//1 实例化四个售票员,用4个线程模拟4个售票员
Runnable r = () -> {
while (TicketCenter.restCount > 0) {
//对象锁 括号里可以是任意对象
//类锁 例如SynchronizedDemo.class
//需要保证一点:多个线程看到的锁,需要是同一把锁
synchronized (""){
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + --TicketCenter.restCount);
}
}
};
Thread t1 = new Thread(r, "售票员1");
t1.start();
Thread t2 = new Thread(r, "售票员2");
t2.start();
Thread t3 = new Thread(r, "售票员3");
t3.start();
Thread t4 = new Thread(r, "售票员4");
t4.start();
}
}
此时得到输出结果,发现顺序的问题已经解决了,但是出现了负数的情况。
这是因为还剩一张票时,线程1、2、3、4都已经进入了while循环,线程1强夺到了然后上锁,等到线程1执行结束,线程2、3、4还是可以获得锁继续执行。
为了解决负数的问题,添加一个if语句就可以了
//对象锁
synchronized (""){
if(TicketCenter.restCount <= 0)
return;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + --TicketCenter.restCount);
}
三、同步方法
使用同步方法可以达到同样的效果
//同步方法: 用关键字synchronized 修饰的方法就是同步方法
/**
*同步的方法
*静态方法:同步锁就是类锁当前类.class
*非静态方法:同步锁是this
*/
private synchronized static void sellTicket(){
if(TicketCenter.restCount <= 0)
return;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + --TicketCenter.restCount);
}
四、锁
使用ReentrantLock实例化一个锁
//实例化一个锁对象
ReentrantLock lock = new ReentrantLock();
对需要上锁的部分加上.lock()方法和.unlock()方法
//对临近资源上锁
lock.lock();
if (TicketCenter.restCount <= 0)
return;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + --TicketCenter.restCount);
//对临界资源解锁
lock.unlock();
可以达到和上面的同步代码段、同步方法同样的效果
死锁
在这个例子中,线程A强夺到了A锁,同时线程B强夺到了B锁,但是程序还没有结束,因为线程A要等待B锁释放后,去强夺B锁继续执行后面的代码,但没有释放自己的锁;线程B也在等待A锁释放,而不释放自己的锁。所以产生了死锁
public class DeadLock {
public static void main(String[] args) {
//死锁:多个线程彼此持有对方所需要的锁对象,而不释放自己的锁。
Runnable r1 = ()->{
synchronized ("A"){
System.out.println("A线程持有了A锁,等待B锁");
synchronized ("B"){
System.out.println("A线程持有了A锁,也持有了B锁");
}
}
};
Runnable r2 = ()->{
synchronized ("B"){
System.out.println("B线程持有了B锁,等待A锁");
synchronized ("A"){
System.out.println("B线程持有了B锁,也持有了A锁");
}
}
};
Thread ta = new Thread(r1,"ThreadA");
Thread tb = new Thread(r2,"ThreadB");
ta.start();
tb.start();
}
}
输出结果:
A线程持有了A锁,等待B锁
B线程持有了B锁,等待A锁
wait和notify
- wait:
等待,object类中的方法,让当前线程释放锁标记,并且让出CPU原则,进入等待队列 - notify:
通知,唤醒等待队列中的一个线程(由cpu确定),使这个线程进入锁池 - notifyAll:
通知,唤醒等待队列中等待特定锁的全部线程,使线程进入锁池
public class DeadLock2 {
public static void main(String[] args) {
//死锁:多个线程彼此持有对方所需要的锁对象,而不释放自己的锁。
Runnable r1 = ()->{
synchronized ("A"){
System.out.println("A线程持有了A锁,等待B锁");
//线程A释放A锁,进入等待队列
try {
"A".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized ("B"){
System.out.println("A线程持有了A锁,也持有了B锁");
}
}
};
Runnable r2 = ()->{
synchronized ("B"){
System.out.println("B线程持有了B锁,等待A锁");
synchronized ("A"){
System.out.println("B线程持有了B锁,也持有了A锁");
"A".notifyAll();//完成任务后释放
}
}
};
Thread ta = new Thread(r1,"ThreadA");
Thread tb = new Thread(r2,"ThreadB");
ta.start();
tb.start();
}
}
输出结果:
A线程持有了A锁,等待B锁
B线程持有了B锁,等待A锁
B线程持有了B锁,也持有了A锁
A线程持有了A锁,也持有了B锁
线程池
每一个线程的启动和结束都是比较消耗时间和占用资源的。如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。为了解决这个问题,引入线程池这种设计思想。
线程池设计思路
线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的任务。
- 准备一个任务容器
- 一次性启动10个 消费者线程
- 刚开始任务容器是空的,所以线程都wait在上面。
- 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
- 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
- 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。
在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程
java自带的线程池
第一个参数10 表示这个线程池初始化了10个线程在里面工作
第二个参数15 表示如果10个线程不够用了,就会自动增加到最多15个线程
第三个参数60 结合第四个参数TimeUnit.SECONDS,表示经过60秒,多出来的线程还没有接到活儿,就会回收,最后保持池子里就10个
第四个参数TimeUnit.SECONDS 如上
第五个参数 new LinkedBlockingQueue() 用来放任务的集合
execute方法用于添加新的任务
public class TestThread {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
threadPool.execute(new Runnable(){
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("任务1");
}
});
}
}