目录
通过wait 方法进入等待的线程被唤起后进入锁池状态的原因:
创建线程的三种方式
通过类继承Thread类
根据要求抽象出一个需要被多线程操作的类,让该类继承Thread类,并重写run方法。在测试类的主方法中正常创建该类对象,通过对象名.start()调用。在创建线程时,可以向内传入一个字符串类型参数,用于设定,该线程的名字,之后通过Thread.currentThread().getName()方法可进行调用,其中Thread.currentThread()是获取当前线程对象。
此方法弊端较为明显,每实现一个线程,都要创建一个该类对象,但是这几个对象的资源并不共享,若希望共享,需要将资源设为静态
下面以多个窗口售票的例子进行理解
售票窗口类:
public class ExtendSell extends Thread{
private int ticket=10;
public ExtendSell(String name) {
super(name);
}
@Override
public void run() {
while (ticket >= 1) {
System.out.println(Thread.currentThread().getName() + "卖出倒数第" + (ticket--) + "张");
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
ExtendSell s1=new ExtendSell("窗口1");
ExtendSell s2=new ExtendSell("窗口2");
ExtendSell s3=new ExtendSell("窗口3");
s1.start();
s2.start();
s3.start();
}
运行截图:
同过类实现Runnable接口
同样根据要求抽象出一个需要被多线程操作的类,此方法是让该类实现接口Runnable,在调用多线程时,与方法一有所区别,需要先创建该类对象,然后Thread 创建线程名=new Thread(刚才创建的对象名);可以通过同一个对象名多次创建线程,这样就会有多个线程,且这几个线程的资源(属性共享),无需像方法1一样,将属性设置为静态
售卖窗口类:
public class ImplementsSell implements Runnable {
int i = 100;
@Override
public void run() {
for (;i>=0;){
System.out.println(Thread.currentThread()+"sell第"+(i--)+"张票");
}
}
}
测试:
public class Test {
public static void main(String[] args) {
ImplementsSell sell=new ImplementsSell();
Thread thread1=new Thread(sell);
Thread thread2=new Thread(sell);
Thread thread3=new Thread(sell);
thread3.start();
thread2.start();
thread1.start();
}
运行截图:
可以看出,此方法还有并发问题。出现原因是当前代码块还未执行完,但系统所分配的CPU使用时间已经结束,需要调度给其他进程,直到再次轮到此进程,才会继续进行刚才未完成的代码块。
通过类实现Callable接口
此种方法解决了方法一二的两个问题,没有返回值 和 不能抛出异常,但在调用的时候会多一个步骤
同时实现的也不再是run方法,而是call方法,虽然名字不同,但作用相同。
同样是需要根据要求抽象出一个需要被多线程操作的类,此方法是让该类实现接口Callable<>,<>中要写的是run方法的返回值类型,不写默认为Object类。
主方法中调用时,需要先创建该类对象,然后根据该对象,创建FutureTask对象,再根据FutureTask对象创建线程。
售卖窗口类:
public class CallableSell implements Callable<String> {
int countnum;
int ticket=100;
@Override
public String call() throws Exception {
for (int i=0;i<1000;i++){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售出" + (++countnum) + "张票,还剩" + (--ticket) + "张票");
}
}
return "线程" + Thread.currentThread().getName() + "执行结束";
}
}
测试类:
public class Test {
public static void main(String[] args) {
CallableSell csell=new CallableSell();
FutureTask task1=new FutureTask<>(csell);
FutureTask task2=new FutureTask<>(csell);
FutureTask task3=new FutureTask<>(csell);
Thread th1=new Thread(task1,"窗口1:");
Thread th2=new Thread(task2,"窗口2:");
Thread th3=new Thread(task3,"窗口3:");
th1.start();
th2.start();
th3.start();
}
}
运行结果:
同样,有并发问题
========================分割==================================
解决并发问题():
通过上面的例子我们能过够看到,输出语句是乱序的,出现原因是当前代码块还未执行完,但系统所分配的CPU使用时间已经结束,需要调度给其他进程,直到再次轮到此进程,才会继续进行刚才未完成的代码块。若是判断条件没有加好,甚至可能会出现负数,而为了解决这个问题,就引出了线程安全问题。
认识synchronized ()---同步与同步监视器
synchronized
关键字的中文代称是“同步”,括号内的中文代称是“同步监视器”。
当某个线程进入了一个被synchronized
修饰的方法或者代码块时,它会尝试获取同步监视器的锁。如果同步监视器的锁已经被其他线程获取了,那么这个线程就会被阻塞,直到获取到锁为止。
通过使用同步监视器,我们可以确保在同一时间只有一个线程可以进入被synchronized
修饰的方法或者代码块,从而保证了线程安全性。
当我们使用synchronized
关键字来修饰一个方法或者代码块时,需要指定一个同步监视器(也称为锁对象)。这个同步监视器可以是任意对象,但通常情况下会选择当前类的实例对象即(this)或者静态对象作为同步监视器。
具体来说:
1.对于实例方法,同步监视器是调用该方法的实例对象。
public synchronized void doSomething() {
// 这里的同步监视器是调用doSomething方法的实例对象
}
2.对于静态方法,同步监视器是当前类的Class对象。
public static synchronized void doSomethingStatic() {
// 这里的同步监视器是当前类的Class对象
}
3.对于代码块,可以使用任意对象作为同步监视器。
但一般为this和当前类名.class。区别在于如果是多个线程共用一个该类对象(如通过实现Runnable接口来实现多线程),用this;
如果是多个线程使用多个该类对象(如通过继承Thread类实现多线程),就需要用类名.class获取其字节码信息对象。这两种都是为了确保多个线程使用的是同一把锁。
Object lock = new Object();
synchronized(lock) {
// 这里的同步监视器是lock对象
}
需要说明的是
多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块。 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块
同步监视器的具体运行流程为:
1)第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
2)第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
3)第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
4)第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
通过同步代码块
通过方法synchronized ()实现,具体实现方法为,将需要被完整执行的代码块用{}包裹起来,然后再左大括号之前加上该关键字,有点类似while循环的格式,()中填入的是就是锁(同步监视器),关于这个锁有以下几点要求
1)必须是引用数据类型,不能是基本数据类型
2)也可以创建一个专门的同步监视器,没有任何业务含义
3)一般使用共享资源做同步监视器即可
4)在同步代码块中不能改变同步监视器对象的引用 ,即不能重新赋值,所以一般建议,这个锁用final来修饰
5)尽量不要String和包装类Integer做同步监视器
以多个窗口售票(实现Callable接口)为例,来理解
public class CallableSell implements Callable<String> {
int countnum;
int ticket=100;
@Override
public String call() throws Exception {
for (int i=0;i<1000;i++){
synchronized (this) {//将判断与输出锁到一起
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售出" + (++countnum) + "张票,还剩" + (--ticket) + "张票");
}
}
}
return null;
}
}
测试方法与创建线程中的 通过类实现Callable接口 实现多线程一致
输出结果:
可以看出,已经能够按序输出
通过同步方法
通过同步方法实现与通过同步代码块类,只不过一个是把锁放在了方法外,一个是放在代码块外,本质一样。
而且
对于普通方法:public synchronized void buyTicket(){}--------锁(同步监视器)是:this
对于静态方法:public static synchronized void buyTicket(){}----锁(同步监视器)是: 当前类.class
还是以多个窗口售票(实现Callable接口)为例,来理解
public class CallableSell implements Callable<String> {
int countnum;
int ticket=100;
@Override
public String call() throws Exception {
for (int i=0;i<1000;i++){
buyticket();
}
return null;
}
public synchronized void buyticket(){
if (countnum <1000) {
System.out.println(Thread.currentThread().getName() + "售出第" + (++countnum) + "张票");
}
}
}
输出结果
关于同步方法要注意:
1)不要将run()定义为同步方法
2)非静态同步方法的同步监视器是this
静态同步方法的同步监视器是 类名.class 字节码信息对象
3)同步代码块的效率要高于同步方法
原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了 代码块的外部,但是却是方法的内部
4)同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法(因为都是默认当前类,即都是this);同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
通过Lock锁
关于LOCK锁,是JDK1.5后新增新一代的线程同步方式;与采用synchronized相比,lock可提供多种锁方案,更灵活。
而且synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成。是虚拟机级别的。 但是Lock锁是API级别的(即写成了类),提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式。
用法为先创建一个锁对象,Lock lock = new ReentrantLock();
然后再要锁的代码块开始的位置加上lock.lock();再结束位置加上lock.unlock();
public class CallableSell implements Callable<String> {
int countnum;
Lock lock = new ReentrantLock();
@Override
public String call() throws Exception {
for (int i=0;i<1000;i++){
lock.lock();
//这里可以加上try-catch,然后将unlock放在finally中
if (countnum <1000) {
System.out.println(Thread.currentThread().getName() + "售出第" + (++countnum) + "张票");
}
lock.unlock();
}
return null;
}
}
一般会对锁的位置加上try-catch-finally,用于即便发生异常,也能让锁解开,而不是一直被锁着。
运行结果:
小结
Lock和synchronized的区别
1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
建议优先使用顺序:
Lock---->同步代码块(已经进入了方法体,分配了相应资源)---->同步方法(在方法体之外)
====================================分割=================================
线程通信(wait和notify)
原理解释:
在Java对象中,有两种池
琐池-------------synchronized
等待池---------------------wait(),notify(),notifyAll()
如果一个线程调用了某个对象的wait方法,那么该线程进入到该对象的等待池中(并且已经将锁释放),如果未来的某一时刻,另外一个线程调用了相同对象的notify方法或者notifyAll方法,那么该等待池中的线程就会被唤起,然后进入到对象的锁池裏面去获得该对象的锁,如果获得锁成功后,那么该线程就会沿着wait方法之后的路径继续执行。
注意:
1.wait方法和notify方法 是必须放在同步方法或者同步代码块中才生效的 (因为在同步的基础上进行线程的通信才是有效的)
2.sleep和wait的区别:sleep进入阻塞状态没有释放锁,wait进入阻塞状态但是同时释放了锁
通过wait 方法进入等待的线程被唤起后进入锁池状态的原因:
当一个线程调用wait方法时,它会释放对象的锁,这意味着其他线程可以获得该对象的锁并执行同步方法或同步块。当另一个线程调用notify或notifyAll方法时,等待中的线程会被唤醒,但在进入就绪状态之前,它需要重新获得对象的锁。
这里所说的锁指的是对象级别的锁,也称为监视器锁或内置锁。在Java中,每个对象都有一个关联的监视器锁,可以通过synchronized关键字来获取对象的锁。当一个线程进入synchronized方法或块时,它会尝试获取对象的监视器锁。在调用wait方法后,线程会释放这个锁,允许其他线程进入同步方法或块。当线程被唤醒后,它需要重新获取对象的监视器锁,这样才能继续执行同步方法或块。
因此,在被唤醒后,线程需要重新获取对象的监视器锁,这个锁就是指对象级别的锁,也称为内置锁。获取了锁之后,线程才能进入就绪状态,等待CPU调度执行。
通过代码理解
如何让两个线程按一定的顺序执行,比如我有两个线程,一个生产者,一个消费者,生产者每生产一个产品,就需要消费者取走一个,然后生产者才能再次生产,如何能让他们实现这种类似有交流的交替效果呢。那便是通过条件判断让线程等待,直到被唤醒。
首先需要抽象出来一个商品类,再该类中实现生产和买两个方法:
生产方法
public synchronized void setProdect(String name,String type){
if (aBoolean){
//---aBoolen用来判断
//如果是flase--消费者等待,直到生产者生产完,改变参数后,被生产者唤醒
//如果是ture--生产者等待,直到消费者购买完,改变该参数后,被消费者进行唤醒
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.type=type;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
this.name=name;
System.out.println("我是生产者,生产了"+this.getType()+this.getName());
aBoolean=true;
//生产者买完后改变参数
notify();//唤醒
}
购买方法
public synchronized void getProdect(){
if (!aBoolean){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("我是消费者,我买走了"+this.getType()+this.getName());
aBoolean=false;
notify();
}
消费者类调用(商品类是其一个属性,且与生产者共用这一个对象)
public void run() {
for (int i = 0; i < 10; i++) {
product.getProdect();
}
}
生产者类调用:
public void run() {
for (int i = 0; i < 10; i++) {
if(i%2==0){
product.setProdect("巧克力","德芙");
}else {
product.setProdect("啤酒","雪花");
}
//这里的if判断是为了能生产两类商品,使通信效果更明显
}
运行结果:
完整代码(分为四个类,商品,生产者,消费者,和测试类)(已删除部分没有用到的get,set,以及构造方法):
/**
* 商品类
*/
public class Product {
private String type;
private String name;
boolean aBoolean=false;
/**
* flase--消费者等待,生产者生产
* ture--生产等待,消费者购买
*/
public Product() {
}
public String getType() {
return type;
}
public String getName() {
return name;
}
public synchronized void setProdect(String name,String type){
if (aBoolean){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.type=type;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
this.name=name;
System.out.println("我是生产者,生产了"+this.getType()+this.getName());
aBoolean=true;
notify();
}
public synchronized void getProdect(){
if (!aBoolean){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("我是消费者,我买走了"+this.getType()+this.getName());
aBoolean=false;
notify();
}
}
/**
* 消费者线程
*/
public class Consumer extends Thread{
private Product product;
public Consumer(Product product) {
this.product = product;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
product.getProdect();
}
}
}
/**
* 生产者线程
*/
public class Maker extends Thread {
private Product product;
public Maker(Product product) {
this.product = product;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
product.setProdect("巧克力", "德芙");
} else {
product.setProdect("啤酒", "雪花");
}
}
}
}
/**
*测试类
*/
public class Test {
public static void main(String[] args) {
Product product=new Product();
Maker maker=new Maker(product);
Consumer consumer=new Consumer(product);
maker.start();
consumer.start();
}
}
其余零散知识点:
程序,进程,线程
程序:静态
进程:动态
线程:同时执行
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。 (程序是静态的)
进程(process):是程序的一次执行过程。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。 (进程是动态的)是一个动的过程 ,进程的生命周期 : 有它自身的产生、存在和消亡的过程
线程(thread),进程可进一步细化为线程, 是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。
并发与并行
并行:多个CPU同时执行多个任务
并发:一个CPU“同时”执行多个任务(采用时间片切换)
线程常见方法
start() : 启动当前线程,表面上调用start方法,实际在调用线程里 面的run方法
run() : 线程类 继承 Thread类 或者 实现Runnable接口的时候,都要 重新实现这个run方法,run方法里面是线程要执行的内容
currentThread :Thread类中一个静态方法:获取当前正在执行的线程
setName 设置线程名字
getName 读取线程名字
程序优先级:
线程的优先级通过setPriority()方法设置,默认为5,最高为10。优先级高的,被调度的可能性会大些。但依旧有很大的随机性。
join方法
直接通过线程对象调用,优先执行调用该方法的线程,抢占cpu资源,其他线程会被阻塞
需要线程先被启动,即先start
sleep
谁调用谁休眠,以毫秒为单位1秒=1000毫秒
需要异常处理try-catch
只能通过Thread打点调用
setDaemon
伴随线程:主线程死亡,伴随线程也要死亡
线程对象名打点调用,谁调用谁是伴随线程,再那个线程里调用谁就是主线程
stop
线程死亡(直接终止线程)---不推荐使用
线程的生命周期
线程通过wait()进入等待队列被唤醒后进入锁池状态的原因见线程通信部分