java多线程的学习之路(二)

笔记来源
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

  1. wait:
    等待,object类中的方法,让当前线程释放锁标记,并且让出CPU原则,进入等待队列
  2. notify:
    通知,唤醒等待队列中的一个线程(由cpu确定),使这个线程进入锁池
  3. 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锁


线程池

笔记来源

每一个线程的启动和结束都是比较消耗时间和占用资源的。如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。为了解决这个问题,引入线程池这种设计思想。

线程池设计思路

线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的任务。

  1. 准备一个任务容器
  2. 一次性启动10个 消费者线程
  3. 刚开始任务容器是空的,所以线程都wait在上面。
  4. 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
  5. 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
  6. 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。

在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程
在这里插入图片描述
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");
            }
               
        });
   
    }
   
}
原文地址:https://www.cnblogs.com/csyxsy/p/13340209.html