【Java笔记】(五):多线程

进程与线程

进程是正在运行的程序,是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源

线程是进程中单个顺序控制流,是一条执行路径,根据执行路径的数量不同,分为单线程和多线程

多线程的实现方式

方法一:继承Thread类(不常用)

  • 继承Thread
  • 重写run方法

两个问题:

为什么要重写run方法?

因为run方法是用来封装被线程执行的代码的

run方法和start方法的区别?

  • run():封装线程执行的代码,直接调用,相当于普通方法
  • start():启动线程,然后由JVM调用此线程的run()方法
//继承Thread类
public class MyThread extends Thread {
    
    
    //重写run方法
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
    
    
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();

        //发现仍然是单线程,因为直接调用run方法其实并没有启动线程
        //myThread1.run();
        //myThread2.run();

        myThread1.start();
        myThread2.start();
    }
}

方法二:实现Runnable接口

  • 实现Runnable接口
  • 实现run方法
  • 创建Thread的时候当作参数传进入
public class MyRunnable implements Runnable {
    
    
    //重写run方法
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }

    public static void main(String[] args) {
    
    
        MyRunnable myRunnable = new MyRunnable();
        Thread thread1 = new Thread(myRunnable,"猴子");
        Thread thread2 = new Thread(myRunnable,"大象");

		//使用匿名内部类的形式
        Thread thread3 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 100; i++) {
    
    
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        };
        thread3.setName("老虎");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

实现Runnable接口的好处

  • 避免了Java单继承的局限性
  • 适用于多个相同程序的代码去处理同一个资源的情况,把线程和程序代码、数据有效分离开,体现了面向对象的设计思想
  • 而且通过实现接口可以使用匿名内部类轻松创建

设置和获取线程的名称

Thread类提供了两个方法:

  • setName()
  • getName()
  • super(name):通过构造方法往上传

获取当前正在执行的线程:

  • Thread提供了currentThread()静态方法

线程优先级

Java实现的是抢占式调度模型,即优先级高的线程可以获得的CPU时间片段多一点。除此之外还有分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片

  • getPriority()获取线程优先级
  • setPriority()设置线程优先级
//继承Thread类
public class MyThread extends Thread {
    
    
    //重写run方法
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
    
    
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        MyThread myThread3 = new MyThread();

        //输出了3个5,即默认为5
        System.out.println(myThread1.getPriority());
        System.out.println(myThread2.getPriority());
        System.out.println(myThread3.getPriority());
        //设置优先级,越大越高
        myThread1.setPriority(11);
        
        myThread1.start();
        myThread2.start();
        myThread3.start();
    }
}

线程控制函数

方法 说明
sleep() 让当前线程暂停指定毫秒数
join() 等待这个线程执行完,剩下的程序才会执行
setDaemon(boolean) 标记为守护线程

注:当运行的线程都是守护线程时,JVM将退出

线程生命周期

在这里插入图片描述

线程数据同步

多线程的同步问题指的是多个线程同时修改一个数据的时候,可能导致的问题 ,多线程的问题,又叫Concurrency问题,解决方法是把操作共享数据的代码锁起来,让任意时刻只能有一个线程执行

问题出现的原因:

  • 多线程环境
  • 有共享数据
  • 有多条执行语句操作共享数据

代码格式:相当于给代码加锁,任意对象可以看成一把锁

synchronized (任意对象) {
    
    
    多条语句操作共享数据的代码
}
public class MyRunnable implements Runnable {
    
    
    private int number = 100;
    private Object obj = new Object();
    //重写run方法
    @Override
    public void run() {
    
    
        while (true) {
    
    
            synchronized (obj) {
    
    
                if (number>0) {
    
    
                    try {
    
    
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(number);
                    number--;
                }
            }
        }
    }

    public static void main(String[] args) {
    
    
        MyRunnable myRunnable = new MyRunnable();

        Thread thread1 = new Thread(myRunnable, "窗口1");
        Thread thread2 = new Thread(myRunnable, "窗口2");
        thread1.start();
        thread2.start();
    }
}

数据同步的好处和弊端

  • 好处:解决了多线程数据安全问题
  • 弊端:当线程很多的时候,会浪费资源,导致程序效率很低

同步方法(加了synchronized的方法)

同步方法就是让synchronized修饰的方法,同步方法的锁对象是this,代码格式如下:

public synchronized void test(){
    
    
	//todo
}

静态同步方法的锁是类名.class,即字节码对象

public static synchronized void test(){
    
    
	//todo
}

线程安全类

Hashtable不能存放null,HashMap可以存放 null

Hashtable是线程安全的类,HashMap是非线程安全的

StringBuffer 是线程安全的,StringBuilder 是非线程安全的,StringBuilder 更快且常用,因为不执行同步

Vector是线程安全的类,而ArrayList是非线程安全的,ArrayList更快且常用,因为不执行同步

concurrent包

concurrent包提供了一些高效的映射、有序集合、队列的实现:

ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue

把非线程安全的集合转换为线程安全

借助Collections.synchronizedList,可以把ArrayList转换为线程安全的List,与此类似的,还有HashSet,LinkedList,HashMap等等非线程安全的类

List<Integer> list2 = Collections.synchronizedList(list1);

Lock锁

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作,有两个常用方法:

  • lock()获得锁
  • unlock()释放锁

Lock是接口,不能直接实例化,常采用它的实现类ReentrantLock来实例化,新版本代码如下

public class MyRunnable implements Runnable {
    
    
    private int number = 100;
    private Lock lock = new ReentrantLock();
    //重写run方法
    @Override
    public void run() {
    
    
        while (true) {
    
    
            try {
    
    
                lock.lock();
                if (number>0) {
    
    
                    try {
    
    
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(number);
                    number--;
                }
            } finally {
    
    
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
    
    
        MyRunnable myRunnable = new MyRunnable();

        Thread thread1 = new Thread(myRunnable, "窗口1");
        Thread thread2 = new Thread(myRunnable, "窗口2");
        thread1.start();
        thread2.start();
    }
}

注:通常情况下使用try...finally来包裹操作,防止中间出现问题无法释放锁

生产者-消费者模式介绍

在这里插入图片描述

生产者生产数据放到共享数据区域,消费者从共享数据区域获取数据

方法 说明
wait() 让当前线程等待,直到另一个线程调用notify()或notifyall()
notify() 唤醒正在等待的单个线程
notifyall() 唤醒正在等待的所有线程

注:包含了这些方法的关键字应该有synchronized进行修饰,否则会报错,如下:

public synchronized void put(int milk) {
    
    
    if (state) {
    
    
        try {
    
    
            wait();//等待
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

生产者-消费者模式代码

奶箱:

public class Box {
    
    
    //表示剩下几瓶奶
    private int milk;
    //表示奶箱的状态
    private boolean state = false;
    //生产奶
    public synchronized void put(int milk) {
    
    
        if (state) {
    
    
            try {
    
    
                wait();//如果有牛奶应该是等待消费
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        //没有牛奶再生产
        this.milk = milk;
        System.out.println("送奶工将第"+this.milk+"放入奶箱");
        state = true;
        //唤醒
        notify();
    }
    //消费奶
    public synchronized void get() {
    
    
        if (!state) {
    
    //没有牛奶就等
            try {
    
    
                wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        //如果有牛奶就消费
        System.out.println("用户拿到第"+this.milk+"奶");
        state = false;
        //唤醒
        notify();
    }
}

生产者:

public class Productor implements Runnable {
    
    
    private Box b;

    public Productor(Box b) {
    
    
        this.b = b;
    }

    @Override
    public void run() {
    
    
        for (int i = 1; i <= 5; i++) {
    
    
            b.put(i);
        }
    }
}

消费者:

public class Customer implements Runnable {
    
    
    private Box b;

    public Customer(Box b) {
    
    
        this.b = b;
    }

    @Override
    public void run() {
    
    
        while (true) {
    
    
            b.get();
        }
    }
}

测试类:

public class BoxTest {
    
    
    public static void main(String[] args) {
    
    
        Box box = new Box();
        Productor productor = new Productor(box);
        Customer customer = new Customer(box);

        Thread thread1 = new Thread(productor);
        Thread thread2 = new Thread(customer);

        thread1.start();
        thread2.start();
    }
}

死锁的产生

  • 线程1 首先占有对象1,接着试图占有对象2
  • 线程2 首先占有对象2,接着试图占有对象1
  • 线程1 等待线程2释放对象2
  • 与此同时,线程2等待线程1释放对象1

就会。。。一直等待下去,直到天荒地老,海枯石烂,山无棱 ,天地合。。。

原子访问

所谓的原子性操作不可中断的操作,比如赋值操作,原子性操作本身是线程安全的 ,但是i++ 这个行为,事实上是有3个原子性操作组成的:

  • 步骤 1. 取 i 的值
  • 步骤 2. i + 1
  • 步骤 3. 把新的值赋予i

JDK6 以后,新增加了一个包java.util.concurrent.atomic,里面有各种原子类,比如AtomicIntege

而AtomicInteger提供了各种自增,自减等方法,这些方法都是原子性的,同一个时间,只有一个线程可以调用这个方法。

public class TestThread {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
    	AtomicInteger atomicI =new AtomicInteger();
    	int i = atomicI.decrementAndGet();
    	int j = atomicI.incrementAndGet();
    	int k = atomicI.addAndGet(3);
    }
}

线程池

每一个任务都去执行一个新的线程,开销还是很大,因此有了线程池,将任务丢到线程池中去执行即可

目前还用不到,等有需要会补充。

猜你喜欢

转载自blog.csdn.net/m0_46521785/article/details/114704990