6.多线程

1 多线程
多线程是提升程序性能非常重要的一种方式,使用多线程可以让程序充分利用CPU资源,提高CPU的使用效率,
从而解决高并发带来的负载均衡问题
优点:
资源得到更合理的利用
程序设计更加简洁
程序响应速度更快,运行效率更高
缺点:
需要更多的内存空间来支持多线程
多线程并发访问可能会影响数据准确性
数据被多线程共享,可能会出现死锁的情况
1.1 进程和线程
进程:计算机正在运行的一个独立的应用程序
线程:线程是组成进程的基本单位,一个进程由一个或多个线程组成的
进程和线程都是应用程序在执行过程中的概念,如果应用程序没有运行,那就谈不上进程和线程的概念,这是一个动态的概念
进程和线程的区别在于进程运行时拥有独立的内存空间,每个进程锁占用的内存都是独立的,互不干扰
多个线程共享内存空间,彼此之间的运行是独立的
1.2 多线程
在一个进程汇总,多个线程同时执行,系统会自动为每个线程分配CPU资源,在某个具体的时间段内,CPU会被一个线程占用,在不同的时间段由不同的线程来使用CPU资源
java
new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println("++++++++++" + i);
}
}).start();
new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println(i + "=============");
}
}).start();
1.3 Java中线程的使用
Thread类:用来创建线程对象的
Runable接口:表示线程要执行的任务
继承Thread类
Thread是JDK提供的一个类,专门用来创建线程对象 ,抽象化的描述,具体使用的时候要实现具体的类
public class MyThread1 extends Thread{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println("张三");
}
}
}
实现Runable对象
耦合度过高,一个线程只能执行一个任务,无法做到任务到线程的自由分配
把线程对象和任务解耦合,将线程和任务分离
创建一个任务
创建一个原生的线程对象,并将任务分配给线程对象
Java
public class MyRunable1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("取快递");
}
}
}
Java
public class test {
public static void main(String[] args) {
MyRunable1 runable1=new MyRunable1();
MyRunable2 runable2=new MyRunable2();
Thread thread1=new Thread(runable1);
Thread thread2=new Thread(runable2);
thread1.start();
thread2.start();
}
}
java
public class test {
public static void main(String[] args) {
new Thread(new MyRunable1()).start();
new Thread(new MyRunable2()).start();
}
}
lambda表达式:函数式编程,可以将方法的具体实现作为参数进行传递
java
public class test {
public static void main(String[] args) {
new Thread(()->{
for(int i=0;i<10;i++){
System.out.println(i);
}
}).start();
}
}
1.4 线程的状态
线程共有5种状态,在特定的情况下,线程可以在不同的状态之间切换,5种状态如下:
创建状态:实例化了一个新的线程队形,还未启动
就绪状态:创建好的线程对象调用了start()方法完成启动,进入线程池等待抢占CPU资源
运行状态:线程对象获取了CPU资源,在一定的时间内执行任务
阻塞状态:正在运行的线程暂停执行任务,释放所占用的CPU资源,并在接触阻塞之后也不能直接回到运行状态,而是重新回到就绪状态,等待获取CPU资源
终止状态:线程运行完毕或因为异常导致线程终止执行
1.5 线程调度
线程休眠:让当前线程暂停执行任务,从运行状态进入阻塞状态,将CPU资源让给其他线程的一种调度方式,通过调用sleep方法来实现,线程休眠需要传入具体的休眠时间
线程合并,将指定的某个线程加入到当前线程中,合并为一个线程,由两个线程交替执行变成一个线程中的两个子线程顺序执行,使用join方法实现
合并之后两个任务编程顺序执行,且优先执行合并进来的任务
Java
public class test {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for(int i=0;i<50;i++){
System.out.println(i +"============");
}
});
thread.start();
for (int i = 0; i < 100; i++) {
if(i == 10){
try {
thread.join();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("++++++++++" + i);
}
}
}
1.6 线程同步
问题:Java中允许多线程并行访问,同一个时间段内多个线程同时完成各自的操作,这样就会带来一个问题,多个线程同时操作一个共享数据的时候,可能会导致数据不准确、
这就需要使用线程同步来解决
异步:多个线程同时执行,不需要排队顺序执行
同步:多个线程按顺序执行,每次只能执行一个线程,执行完之后才能继续下一个线程的执行
就是因为异步导致上述的问题,所以我们要将程序改为同步的模式即可解决问题
如何实现同步呢?用上锁的方式解决
synchronized关键字上锁
java
public class Account implements Runnable{
private static int num;
@Override
public synchronized void run() {
++num;
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"是当前的第" + num + "访客");
}
}
synchronized可以修饰实例方法(非静态,没有static修饰的方法),也可以修饰静态方法,还可以修饰代码块
1.7 线程安全的单例模式
单例模式是一种常见的软件设计模式,核心思想是一个类只有一个实例对象,由多个线程来共享该实例对象的资源
将构造函数私有化
提供共有的静态方法返回实例对象
java
public class SingletonDemo {
private static SingletonDemo instance;
private SingletonDemo(){
System.out.println("创建了一个对象");
}
public synchronized static SingletonDemo getInstance(){
if(instance==null){
instance = new SingletonDemo();
}
return instance;
}
}
1.8 死锁
使用synchronized可以实现线程同步,可以解决多线程并行访问数据带来的安全问题,synchronized在解决线程安全问题的同时也会带来新的问题,死锁。
多个线程争夺同一个资源而带来的互斥问题
例如:10个人围在一桌吃饭,但是每个人只有一个筷子,要求必须凑齐一双筷子才可以吃饭,也就是每个人必须要拿到其他人的筷子,但是每个人又不愿意把自己的筷子让出来给其他人使用,都在等待其他人让出筷子,这样就形成一个四局,如果一直保持这个状态,饭局就会一直僵下去,没有一个人能吃到饭。
如果把每个人都看成一个线程,筷子就是线程要获取的资源,现在每个线程都占用一个资源并且不远亿释放,而且任意一个线程想继续执行就必须获取其他线程的资源,那么所有的线程都处于阻塞状态,程序无法向下执行页无法结束
如何破解死锁呢?只有某个线程愿意作出让步,贡献出自己的资源给其他的线程使用,获取到资源的线程就可以执行自己的任务,执行完毕后就会释放占用的资源,这样其他的线程就可以依次获取资源来执行任务
java
import javax.swing.plaf.TableHeaderUI;
public class DeadLockRunnable implements Runnable {
public int num;
private static Chopsticks chopsticks1 = new Chopsticks();
private static Chopsticks chopsticks2 = new Chopsticks();
@Override
public void run() {
//张三拿到筷子1,李四拿到筷子2
if (num == 1){
System.out.println(Thread.currentThread().getName() + "获取到chopsticks1,等待获取chopsticks2");
synchronized (chopsticks1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (chopsticks2) {
System.out.println(Thread.currentThread().getName() + "用餐完毕");
}
}
}
if (num == 2){
System.out.println(Thread.currentThread().getName() + "获取到chopsticks2,等待获取chopsticks1");
synchronized (chopsticks2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (chopsticks1) {
System.out.println(Thread.currentThread().getName() + "用餐完毕");
}
}
}
}
}
java
public class test {
public static void main(String[] args) {
DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
deadLockRunnable1.num = 1;
DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
deadLockRunnable2.num = 2;
Thread t1 = new Thread(deadLockRunnable1);
t1.setName("张三");
Thread t2 = new Thread(deadLockRunnable2);
t2.setName("李四");
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
最简单的解决方法,不要同时进行,例如这里使用了一个sleep让线程一做完了再来进行线程二
1.9 重入锁
ReentrantLock是对synchronized的升级,synchronized是通过JVM实现的,ReentrantLock是通过JDK实现的
重入锁就是可以重复上锁,可以给同一个资源添加多把锁,并且它的解锁方式和synchronized也不一样,synchronized的锁是线程执行完毕之后会自动释放,ReentrantLock的锁必须手动释放,加了几把锁就要手动解几把锁
java
public class Account implements Runnable{
private static int num;
private ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
reentrantLock.lock();
++num;
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"是当前的第" + num + "访客");
reentrantLock.unlock();
}
}
Java
public class test {
public static void main(String[] args) {
Account account = new Account();
Thread t1 = new Thread(account);
Thread t2 = new Thread(account);
t1.start();
t2.start();
}
}
重入锁还有个限时性的特点
可以判断某个线程在一定时间内能否获取到锁,通过调用tryLock方法实现
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeLock implements Runnable {
public ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
try {
if (reentrantLock.tryLock(3, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
Thread.sleep(5000);
}else {
System.out.println(Thread.currentThread().getName() + "无法获取锁");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
if(reentrantLock.isHeldByCurrentThread()){
reentrantLock.unlock();
}
}
}
}
java
import java.sql.Time;
public class test {
public static void main(String[] args) {
TimeLock lock = new TimeLock();
Thread t1 = new Thread(lock,"线程1");
Thread t2 = new Thread(lock,"线程2");
t1.start();
t2.start();
}
}
1.10 生产者消费者模式
生产者消费者模式是指在一个生产环境中,生产者和消费者在同一个时间段内共享一块缓冲去,生产者负责向缓冲区中添加数据,消费者负责从缓冲区中取出数据
wait:让当前正在访问资源的线程暂停执行
notify:让该线程恢复执行
java
public class Hamburger {
private int id;
public Hamburger(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "Hamburger{" + "id=" + id + '}';
}
}
java
public class Container {
public Hamburger[] hamburgers = new Hamburger[10];
public int index = 0;
public synchronized void addHamburger(Hamburger hamburger) {
while(index == hamburgers.length) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.notify();
hamburgers[index] = hamburger;
index++;
System.out.println("生产了一个汉堡" + hamburger);
}
public synchronized Hamburger removeHamburger() {
while(index == 0){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.notify();
index--;
System.out.println("消费了一个汉堡" + hamburgers[index]);
return hamburgers[index];
}
}
java
import java.util.function.Consumer;
public class Producer implements Runnable {
private Container container = null;
public Producer(Container container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
Hamburger hamburger = new Hamburger(i);
this.container.addHamburger(hamburger);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
java
public class Consumer implements Runnable {
private Container container;
public Consumer(Container container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
this.container.removeHamburger();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
java
import java.sql.Time;
public class test {
public static void main(String[] args) {
Container container = new Container();
Producer producer = new Producer(container);
Consumer consumer = new Consumer(container);
new Thread(producer).start();
new Thread(producer).start();
new Thread(consumer).start();
new Thread(consumer).start();
new Thread(consumer).start();
}
}
1.11 多线程的实际应用
一场球赛的球票分3个窗口出售,共15张票,用多线程模拟3个窗口的售票情况
java
public class TicketRunnable implements Runnable {
//剩余球票
public int count1 = 15;
//已售出球票
public int count2 = 0;
@Override
public void run() {
while(count1 > 0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(count1 == 0){
return;
}
synchronized(TicketRunnable.class){
count1--;
count2++;
if(count1 == 0){
System.out.println(Thread.currentThread().getName() + "售出了第" + count2 + "张票,票已经售尽");
}else{
System.out.println(Thread.currentThread().getName() + "售出了第" + count2 + "张票,还剩余" + count1 + "张票");
}
}
}
}
}
java
import java.sql.Time;
public class test {
public static void main(String[] args) {
TicketRunnable ticketRunnable = new TicketRunnable();
new Thread(ticketRunnable,"窗口A").start();
new Thread(ticketRunnable,"窗口B").start();
new Thread(ticketRunnable,"窗口C").start();
}
}
评论