123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- 程序的并行和并发:
- 并行:在同一个时刻,有多个指令在多个CPU(运算器+控制器--计算机核心)上同时执行。
- 并发:在同一个时刻,多个指令在单个CPU上交替执行。
- 程序的进程和线程:
- 进程:就是正在运行的程序。
- 一个程序一旦运行起来,就会通过输入流进入到内存运行起来。变成了进程。
- 独立性:进程是一个能够独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
- 动态性:进程的本质是程序的一次执行过程,进程是动态产生,动态消亡。
- 并发性:任何的进程都可以和其他进程一起并发执行。
- 线程:
- 是进程中的单个顺序控制了,是一条执行路径。
- 单线程:是一个进程中如果只有一条执行路径,就称为是单线程程序。
- 多线程:一个进程中如果有多条执行路径,就称为多线程程序。
- Java中实现线程的方式:
- 方式一:继承Thread类
- 方法:
- void run() 当线程开启后,该方法会被调用执行。
- 是从Thread类继承过来的,目的是让子类重写。
- void start() 让此线程开始执行,Java虚拟机会自动调用run()方法。
- 实现步骤:
- 1、定义一个类继承Thread类,
- 2、在子类中重写Thread类中的run()方法
- 3、创建子类的对象
- 4、调用start()方法
- 方式二:实现Runnable接口
- 实现步骤:
- 1、定义一个类实现Runnable接口
- 2、在实现类中重写run方法
- 3、创建Thread类的对象,将实现类对象作为构造方法的参数传入
- 4、调用Thread类的start方法
- 方式三:实现Callable接口-创建带有返回值的线程
- 方法:
- V call() 计算结果,如果无法计算结果则抛出一个异常。
- FutureTask(Callable<V> callable)
- 创建一个FutureTask,一旦运行,就会执行传入的参数Callable对象的call方法。
- V get() 如果又必要,则等待计算完成,然后获取结果。
- 实现步骤:
- 1、定义一个类实现Callable接口,并且可以指定泛型
- 2、在实现类中重写call()方法
- 3、创建实现类的对象
- 4、创建FutureTask对象,把Callable的实现类的对象作为FutureTask类的构造方法的参数
- 5、创建Thread类的对象,把FutureTask对象作为Thread类的构造方法的参数
- 6、调用Thread对象的start方法,启动线程
- 7、再调用Future接口的get方法(实际上就是FutureTask对象的get方法),获取线程结束后的计算结果。
- 线程的方法:
- void setName(String name) 可以将该线程的名称改为name参数
- String getName() 获取线程名称
- Thread currentThread() 获取当前正在执行的线程对象的引用。
- 线程休眠:
- static void sleep(long ms) 让当前的线程停留(暂停执行)指定的毫秒数。
- 线程的生命周期:
- 什么是生命周期? 从生到死的过程。 怎么来滴?怎么没滴?
- 线程优先级:
- 线程有两种调度方式:
- 分时调度模型:所有的线程轮流使用CPU的使用权,平均分配每个线程占用的时间片。
- 抢占式调度模型:
- 优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就会随机选择一个,
- 并且优先级比较高的线程获取的时间片也相对多一些。
- ps: java使用的就是抢占式调度模型。
- 随机性问题:假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一个指令,线程只有得到了时间片才能得到使用权,
- 才可以执行指令。所以多线程的执行是随机性,也就是谁能抢到执行权是不一定的。
- final int getPriority() 返回线程的优先级
- final void setPriority(int newPriority) 修改当前线程的优先级,线程的默认优先级是5,优先级的范围:1~10;
- MAX_PRIORITY 10 最高优先级
- MIN_PRIORITY 1 最低优先级
- 守护线程
- 守护线程随着其他非守护线程的结束而结束。
- 设置守护线程的方法:void setDaemon(boolean on)
- 卖票问题:
- - 案例需求
- 某电影院目前正在上映国产大片《南京照相馆》,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
- - 实现步骤
- - 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
- - 在SellTicket类中重写run()方法实现卖票,代码步骤如下
- - 判断票数大于0,就卖票,并告知是哪个窗口卖的,卖1张票前,sleep(1),模拟出票过程。
- - 卖了票之后,总票数要减1
- - 票卖没了,线程停止
- - 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下
- - 创建SellTicket类的对象
- - 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
- - 启动线程
- 卖票出现了问题:
- 1、相同的票出现了多次
- 2、出现了负数票
- 原因:为什么出现上面的问题?
- 线程执行的随机性导致,在卖票的过程中丢失了CPU的执行权,导致的问题。
- 那么就需要解决多个线程的同步问题,保证线程的数据一致性,线程安全。
- 这种问题称为线程的安全问题:
- 1、有多个线程
- 2、线程之间的数据是共享的。
- 3、有多个线程操作共享数据。
- 实现解决安全问题的方式:
- 1、把多条语句操作共享数据的代码锁起来。让任何时刻都只有一个线程能执行。
- 2、Java提供了同步代码块的方式解决问题
- 解决线程安全的方式:
- 1、同步代码块 synchronized代码块
- 2、同步方法 synchronized修饰的方法
- 3、可重入锁(Lock) ReentrantLock锁
- 1、同步代码块:
- synchronized(任意对象){
- 多条语句操作共享数据的代码
- }
- synchronized(任意对象)就相当于给代码加锁了,任意对象就可以看成是一个所。
- 好处:解决多线程数据安全的问题。
- 弊端:当线程很多的守护,因为每个线程都要判断同步锁,比较耗费资源,会降低程序的执行效率。
- 2、同步方法:
- 就是把synchronized关键字加到方法上
- 格式:
- 访问权限修饰符 synchronized 返回值类型 方法名(参数){
- 方法体
- }
- 同步方法里的锁对象是当前的对象 this
- 静态同步方法格式:
- 访问权限修饰符 static synchronized 返回值类型 方法名(参数){
- 方法体
- }
- 静态同步方法锁对象是这个类 类名.class的对象
- 同步监视器:
- 俗称就是锁,代码块中任何一个类的对象都可以当成锁,但是所有的线程必须要共用同一把锁,共用同一个对象
- 如果是同步方法中,这个锁就是this,也就是方法所属对象,静态同步方法的锁是类本身。
- 如果是使用同步代码块,恰好使用继承Thread的方式创建线程,那么一定一定不能使用this作为锁。
- 3、ReentrantLock锁(可重入锁)
- Lock类本身是一个接口,他有一个实现类ReentrantLock,我们可以创建实现类对象
- 包含两个常用的方法:
- 获得锁 void lock()
- 释放锁 void unlock()
- lock锁和synchronized同步代码块的相同点和不同点
- 1、Lock锁是一种显式锁,需要我们手动的开启和关闭,synchronized是隐式锁,出了作用域之后会自动释放。
- 2、Lock锁只有代码块锁,synchronized包含代码块锁和方法锁
- 3、使用Lock锁,Java虚拟机JVM可以花费更少的时间来调度线程,性能更好,并且具有更高的扩展性。
- 死锁:
- 线程的死锁是由于两个或多个线程互相持有对方需要的资源,导致这些线程都处在等待状态,不能继续执行。
- 产生死锁的原因:
- 1、资源有限
- 2、同步嵌套
- 需要做的是避免死锁。
- 出现死锁后并不会出现异常,也不会出现提示,只是所有的线程都在正常运行,只是都没在工作,而是都在被阻塞而已。
- 所以使用线程同步的时候,一定要避免死锁。
- 解决方案:
- 1、设计专门的算法,原则。
- 2、尽量减少同步资源的定义。
- 3、尽量避免嵌套同步。
- List集合的实现类
- ArrayList LinkedList
- Set集合的实现类
- HashSet TreeSet
- Map集合的实现类
- HashMap TreeMap
- 以上6个集合实现类都不是线程同步的,都是线程不安全的。
- 怎么样让以上的集合变为线程安全的?
- 1、使用synchronized进行二次封装
- 2、使用可重入锁。
- 3、使用Collections工具类,该类里有把线程不安全的集合封装成线程安全的集合的方法。
- 题目 1:多线程操作 HashSet 的线程安全问题
- 需求:
- 创建 3 个线程,同时向一个 HashSet 中添加 1-100 的整数(每个线程负责不同区间,如线程 1 加 1-33,线程 2 加 34-66,线程 3 加 67-100),观察最终集合大小是否为 100(可能出现重复添加),然后使用 Collections.synchronizedSet () 包装 HashSet,确保线程安全并验证结果。
- 题目 2:多线程向 TreeSet 添加元素并排序
- 需求:
- 创建 2 个线程,线程 1 向 TreeSet 中添加 10 个随机偶数,线程 2 添加 10 个随机奇数(范围 1-100),所有线程执行完毕后,遍历 TreeSet 输出元素,验证是否自动按自然顺序排序且无重复。
- 题目 3:多线程并发查询 HashSet
- 需求:
- 创建一个包含 50 个字符串的 HashSet,启动 4 个线程,每个线程循环 100 次随机查询集合中是否包含某个字符串(从集合元素中随机选取),使用同步机制确保查询过程中集合不被修改,统计每个线程的查询成功次数。
|