java从零开始之多线程操作

线程创建

java无法真正开启线程,只能通过private native void start0(); 调用本地方法.让本地c++方法开启线程,因为java运行在虚拟机上无法直接操作硬件.

继承Thread类

继承Thread类,重写run方法,创建对象,调用start方法开启线程执行run方法.
在这里插入图片描述
start()方法开启的子线程不一定立即执行,由CPU决定调度那个线程

实现Runnable接口

外部类

public class TalkSend implements Runnable{
    //xxx
    @Override
    public void run(){
       // xxx
    }
}
public class TalkReceive implements Runnable{
    //xxx
    @Override
    public void run(){
       // xxx
    }
}

//开启两个线程
new Thread(new TalkSend(5555,"localhost",8888)).start;
new Thread(new TalkReceive(9999,"学生")).start;

内部类

   new Thread(new Runnable() {
        @Override
        public void run() {
            try {
               Thread.sleep(3000);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
             }
        }
    }).start();

真实开发中,类不用实现接口,直接new线程将对象传入

//资源类不需要实现Runnable接口,重写run方法. 完成解耦
class Ticket{
    //正常编写属性和方法
    private int num =50;
    public void sale(){
        //.....
    }
}


public static void main(String[] args){
    Ticket tk = new Ticket();
    //使用lambda表达式,启动线程    
    new Thread(()-> {tk.sale();},"线程A").start();
    new Thread(()-> {tk.sale();},"线程B").start();
    new Thread(()-> {tk.sale();},"线程C").start();
}

实现Callable接口

有返回值(和指定的泛型相同),可以抛出异常

  1. 实现callable接口(要指定泛型),
  2. 重写call方法.
  3. 因为Thread不能直接 接收Callable接口,所有要通过FutureTask适配类. FutureTask即可接收Callable接口,也可接收Runnable接口
  4. FutureTask的 get 方法获取Callable接口call方法的返回值.
    可能会因为call方法没执行完而阻塞

在这里插入图片描述在这里插入图片描述

注意: 开启了两个线程但因为结果会被缓存,所以call()方法只执行了一次.

Thread方法

//构造器
new Thread(Runnable接口实现类对象);
new Thread(Runnable接口实现类对象,线程名);

//非静态方法
start();   //创建线程
setPriority(int)  //更改线程优先级
getPriority()   //获取线程优先级
a.join()     //等待a线程运行结束,再继续执行当前线程
isAlive()  //检测线程是否处于活跃状态
interrupt()   //中断线程,不推荐使用
getName()  //获取线程名

//静态方法
Thread.currentThread()   //获取当前的线程
Thread.sleep(200)    //线程休眠200毫秒,不会释放锁
Thread.yield()     //线程让出CPU,再共同竞争

线程状态Thread.State

在这里插入图片描述

线程优先级priority

优先级用数字表示1-10,数字越大优先级越高,main方法默认为5

注意: 先设置优先级再启动

守护线程daemon

  • 虚拟机不需要等待守护线程执行完毕
  • 虚拟机必须等待用户线程执行完毕
Thread thread=new Thread(god);
thread.setDaemon(true);  //默认是false表示是用户线程,正常的线程都是用户线程.

停止线程

  • 不推荐使用jdk提供的停止线程方法: stop destroy
  • 推荐让线程自己停下来,使用一个标志位来停止线程,并对外提供一个修改标识的方法.

线程同步

synchronized

锁的范围

  • 非静态方法: 每个对象都有一个锁(对象锁),记录在对象头的Mark Word中.
  • 静态方法: 静态方法在类加载时就生成了,存在于.class字节码文件中,不管 new多少个对象都只有一个锁 (类锁,只锁静态方法,与对象锁互不干扰).

使用方式

  • 同步方法: 在方法的访问权限修饰符后,使用synchronized修饰
  • 同步代码块: synchronized (obj) { }
    • obj为同步监视器,可以是任何对象,但推荐使用共享资源.同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this即对象本身.

注意:当使用String作为监视器时,如果两个字符串相同且后一个不是new 出来的,那么这两个字符串从同一个地址取值,可以看做同一个对象.锁也是共用的,可能导致死锁等问题.

synchronized 优化

  • JVM在JDK 1.6中引入了分级锁机制来优化synchronized
  • 当一个线程获取锁时,首先对象锁成为一个偏向锁
    • 这是为了避免在同一线程重复获取同一把锁时,用户态和内核态频繁切换
  • 如果有多个线程竞争锁资源,锁将会升级为轻量级锁
    • 这适用于在短时间内持有锁,且分锁交替切换的场景
    • 轻量级锁还结合了自旋锁来避免线程用户态与内核态的频繁切换
  • 如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁
  • 优化synchronized同步锁的关键:减少锁竞争
    • 应该尽量使synchronized同步锁处于轻量级锁或偏向锁,这样才能提高synchronized同步锁的性能
    • 常用手段
      • 减少锁粒度:降低锁竞争
      • 减少锁的持有时间,提高synchronized同步锁在自旋时获取锁资源的成功率,避免升级为重量级锁
  • 在锁竞争激烈时,可以考虑禁用偏向锁和禁用自旋锁

LOCK

lock接口的实现类

  • ReentrantLock 可重入锁
    • 空参构造默认创建非公平锁(可插队),
    • 也可传入一个Boolean值指定创建,true代表创建公平锁(先来后到)
  • ReentrantReadWriteLock.ReadLock:读锁
  • ReentrantReadWriteLock.WriteLock:写锁
private final ReentrantLock lock =new ReentrantLock();

//建议将unlock()写到finally语句块中
lock.lock(); //加锁
try{
    //需要保证线程安全的代码
}finally{
    lock.unlock();  //解锁
}

lock.tryLock()  //尝试获取锁

Lock与synchronized的区别

  1. Lock是类由juc包提供,synchronized是关键字jvm实现
  2. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
  3. Lock只有代码块锁,synchronized有代码块锁和方法锁
  4. Lock可以手动尝试获取锁,synchronized没有锁时只能等待
  5. Lock可以判断是否获取到了锁,synchronized不能判断
  6. Lock的定制性更高可设置为公平锁,synchronized则不行

Volatile

Volatile(关键字) 是 Java 虚拟机提供轻量级的同步机制,修饰在变量上,常用于单例模式

  1. 保证内存可见性 :使用Volatile修饰变量,使工作内存的变量副本可以感知到主内存变量的变化
  2. 不保证原子性 :使用atomic原子类或加锁来保证原子性
  3. 禁止指令重排(有序性): 使用内存屏障保证操作的执行顺序,常用在单例模式中.

指令重排

源代码–>编译器优化的重排–> 指令并行也可能会重排–> 内存系统也会重排—> 执行

处理器在进行指令重排的时候,会考虑单线程数据之间的依赖性!
但多线程时,原本无依赖的操作进行重排后可能导致出错

Volatile与synchronized的区别

  1. volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在方法和代码块级别的;
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

线程协作通信

synchronized使用等待wait与唤醒notify

下面是Object类的同步监视器方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常llegalMonitorStateException

wait()  //线程会一直等待,直到其他线程通知.会释放锁
wait(long)  //等待指定的毫秒数
notify() //唤醒任意一个处于等待的线程
notifyAll() //唤醒同一个对象上所有调用wait()方法的线程

注意:
wait() 如果在 if 中执行,当被唤醒时不会重新判断if条件,而直接向下执行代码, 可能导致虚假唤醒.
wait() 如果在 while 中执行,被唤醒后会重新判断循环条件.如果不满足则仍然等待.

lock使用Condition接口中的方法

可以创建多个Condition同步监视器,实现精准唤醒

ReentrantLock lock =new ReentrantLock();
Condition  condition = lock.newCondition();

condition.await();  //等待
condition.signalAll(); //唤醒全部线程
condition.signal();  //唤醒condition监视等待的线程,实现精确唤醒

生产者消费者模型

流程
判断是否等待->业务->唤醒,通知

判断等待要用while,而不能使用if,否则可能产生虚假唤醒.

管程法
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
信号灯法
通过标志位,决定生产者执行还是消费者执行

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值