JAVA基础--多线程、线程安全

线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程:
进程概念
在这里插入图片描述

线程概念
在这里插入图片描述
线程调度:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

创建线程

  • 继承Thread类创建
public class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i=0;i<20;i++) {
    
    
            System.out.println("myThread:" + "i = " + i);
        }
        // Thread类的run方法并没有执行
        /*
        * @Override
          public void run() {
        if (target != null) {
            target.run();
        }
        }*/
        // 因为我们是直接使用该类的start方法,也就是直接调用其父类Thread类的start方法
        // 并没有对Thread类进行创建对象,所以target为空,没有执行该方法,
        // 而是在该类中添加相关指令(for循环)
        super.run();
    }
}

使用多线程
    public static void main(String[] args) {
    
    
        MyThread mt = new MyThread();
        mt.start();
        for (int i=0;i<20;i++) {
    
    
            System.out.println("main:" + "i = " + i);
        }
    }

运行结果:主线程和新线程抢占资源,谁抢到谁运行。
在这里插入图片描述

Thread类的部分方法

  • 构造方法
public Thread() {
    
    
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

public Thread(Runnable target) {
    
    
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
  • run()方法
    该方法是实现Runnable接口的方法
@Override
    public void run() {
    
    
        if (target != null) {
    
    
            target.run();
        }
    }
  • getName()方法:获取线程名字
  • currentThread() 静态方法,获取当前线程
    获取线程名字的俩种方法
public class MyThread extends Thread{
    
    

    public MyThread(String name) {
    
    
        super(name);
    }

    public MyThread() {
    
    
    }

    @Override
    public void run() {
    
    
        // 第一种获取线程名字方法
        /*String name = super.getName();
        System.out.println(name);*/
//        Thread thread = Thread.currentThread();
        // 第二种获取线程名方法
        System.out.println(Thread.currentThread().getName());
        super.run();
    }
}


public class ThreadName {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        MyThread mt = new MyThread("新线程");
        mt.start();
//        new MyThread().start();
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
        /*for (int i = 0; i < 10; i++) {
            System.out.println("i= " + i);
            Thread.sleep(1000);
        }*/
    }
}

在这里插入图片描述

  • sleep():延迟,以毫秒为单位

实现Runnable类创建线程

public class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i=0;i<20;i++) {
    
    
            System.out.println("myThread:" + "i = " + i);
        }
    }
}

public class Demo02_Thread {
    
    
    public static void main(String[] args) {
    
    
        // 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
        MyRunnable mr = new MyRunnable();
        // 这里Thread构造了target对象,所以run方法是调用我们重新接口的run方法
        new Thread(mr).start();
        for (int i=0;i<20;i++) {
    
    
            System.out.println("main:" + "i = " + i);
        }
    }
}

在这里插入图片描述

两种创建方式的区别

可以看到两种方式都是围绕着Thread和Runnable,
继承Thread类把run()写到类中,
实现Runnable接口是把run()方法写到接口中然后再用Thread类来包装, 两种方式最终都是调用Thread类的start()方法来启动线程的。
两种方式在本质上没有明显的区别,在外观上有很大的区别,
第一种方式是继承Thread类,因Java是单继承,如果一个类继承了Thread类,那么就没办法继承其它的类了,
在继承上有一点受制,有一点不灵活,
第二种方式就是为了解决第一种方式的单继承不灵活的问题,所以平常使用就使用第二种方式

使用匿名内部类来创建方法

public class WithoutName {
    
    

    public static void main(String[] args) {
    
    
        // 使用匿名内部类的方式重写run方法
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(Thread.currentThread().getName());
                super.run();
            }
        }.start();

        Runnable r = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 10; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + '-' + i);
                }
            }
        };
        Thread thread = new Thread(r);
        thread.start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 10; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + '-' + i);
                }
            }
        }).start();

    }
}

线程安全

在大多数实际的多线程应用种,两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取同一对象,并且每个线程分别调用了一个修改该对象状态的方法,这两个线程会相互覆盖,可能会导致对象被破坏,这种情况通常 称为竞态条件

模拟电影院多窗口售卖电影票的过程,发现多线程的安全问题。
在这里插入图片描述
MyThread.java

public class MyThread implements Runnable{
    
    

    private int ticket = 100;

    @Override
    public void run() {
    
    
        // 重复卖票
        while (true) {
    
    
            if (ticket> 0) {
    
    
                try {
    
    
                    Thread.sleep(10);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ "正在卖第"+ticket+ "张票");
                ticket--;
            }
        }
    }
}

出现了卖同一张票和卖不存在的票的现象
在这里插入图片描述
在这里插入图片描述问题分析:
卖同一张票分析:1号窗口抢到cpu资源,进入if判断,准备卖第100张票,这时二号窗口抢到了资源,卖出去了第100张票还没执行到ticker–;一号窗口又抢到了资源,又卖第100张票。
买不存在票分析:1号窗口卖了最后一张票,但还未进行ticket–;2号窗口抢到了资源进入if判断还未卖出去呢,1号窗口又抢到了资源进行ticket–;接着2号窗口已经进入if判断了,于是卖了一张不存在的票。

解决方法:
1、锁对象

使用ReentrantLock类来保护代码块,ReentrantLock类继承于Lock.

java.util.conuurrent.locks.Lock

  • void lock()
    获得这个锁,如果当前锁被另外一个线程占有,则阻塞。
  • void unlock()
    释放这个锁

使用如下结构进行加锁

myLock.lock(); // ReentrantLock对象
try
{
    
    
   关键部分
}
fina {
    
    
myLock.unlock();
}

注:一定要把unlock放在finally字句中,否则如果发生异常,会导致该锁没有释放,其他线程将永远阻塞。

解决卖票问题

    private int ticket = 100;
    private ReentrantLock  reentrantLock= new ReentrantLock();
reentrantLock.lock();
@Override
    public void run() {
    
    
        // 重复卖票
        while (true) {
    
    
            if (ticket> 0) {
    
    
                try {
    
    
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName()+ "正在卖第"+ticket+ "张票");
                    ticket--;
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    reentrantLock.unlock();
                }
            }
}

没有出现上面情况
在这里插入图片描述
条件对象
当线程进入临界区后,发现只有满足了某个条件之后它才能执行,可以使用条件对象来管理那些已经获得了一个锁不能做有用工作的线程。

如果不使用条件对象而是直接判断,满足条件再调用方法,当前线程完全有可能被中断。

if(满足条件)
执行方法进行操作

java.util.concurrent.locks.Condition 5

  • void await()
    将线程放在这个条件的等待集中。
  • void signAll()
    从该条件的等待集中随机选择一个线程,解除其阻塞状态。
private Condition sufficientFunds;

mylock.lock();
try{
    
    
	while(不满足条件)
		sufficientFunds.await();// 进入等待集,阻塞状态
	执行操作
	
	sufficientFunds.signAll();// 最后该线程执行完毕,再随机挑选其他线程执行
}

2、同步方法
前面使用的Lock和Condition接口允许程序员充分控制锁定。不过,大多数情况下不需要那样控制,完全可以使用java语言内置的一种机制。
如果一个方法声明时有synchronized关键字,那么对象的锁将保护整个方法,要调用这个方法,线程必须获得内部对象锁。
如下所示:对方法中的步骤进行了加锁,仍然不会出现卖重复票和卖不存在的票的现象。

public synchronized void buyTicket() {
    
    
        if (ticket> 0) {
    
    
            try {
    
    
                Thread.sleep(10);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ "正在卖第"+ticket+ "张票");
            ticket--;
        }
    }

同步方法中使用条件对象直接使用Object类的wait()和notifyAll()方法。默认就是this调用。
3、同步代码块

// 定义对象锁
private Object obj = new Object();

synchronized (obj) {
    
    
                if (ticket> 0) {
    
    
                    try {
    
    
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+ "正在卖第"+ticket+ "张票");
                    ticket--;
                }
            }

同步代码块中使用条件对象需要使用obj.wait(),obj.notifyAll()。
同步方法和同步代码块其实没啥区别,同步代码块可以实现部分代码的上锁更方便。

猜你喜欢

转载自blog.csdn.net/qq_44660367/article/details/109061842