Java多线程基础与线程安全

多线程基础

线程基础知识

java有2个线程:main,GC

java不能开启线程,因为Java无法直接操控硬件,所以实际上是调用了本地方法(C++)去开启线程

什么是进程?

进程是程序在并发环境中的执行过程

什么是线程?

线程是CPU调度的最小单位

进程和线程的区别

  • 进程是操作系统资源分配的基本单位,线程是CPU的基本调度单位

  • 一个程序运行后至少有一个进程

  • 一个进程可以包含多个线程,但是至少需要有一个线程

  • 进程之间不能共享数据段地址,但同进程的线程之间可以


线程的组成

  • CPU时间片:OS会为每个线程分配执行时间
  • 运行数据:
  • ​ 堆空间:存储线程需要使用的对象,多个线程可以共享堆中的对象
  • ​ 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈
  • 线程的逻辑代码

线程的执行特点

  • 抢占式执行:效率高、可防止单一线程长时间独占CPU
  • 在单核CPU中,宏观上同时执行,微观上顺序执行

线程6种状态

6种状态:新建、执行、阻塞、等待、超时等待、终止

public enum State {
    
    // 尚未启动的线程
    NEW,

    // 可执行
    RUNNABLE,

    // 阻塞
    BLOCKED,

    // 等待
    WAITING,

    // 超时等待
    TIMED_WAITING,

    // 终止
    TERMINATED;
}

线程6种状态

  • 注:JDK5之后就绪、运行统称RUNNABLE

创建、使用线程

1、继承Thread类

public class MyThread extends Thread{

    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.start();
        thread2.start();
    }

    @Override
    public void run() {
        for(int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
        }
    }
}

获取线程名字

System.out.println(Thread.currentThread().getName());

获取线程ID

System.out.println(Thread.currentThread().getId());

修改线程名称:第一种方法

MyThread thread1 = new MyThread();
thread1.setName("A");

修改线程名称:第二种方法

public MyThread(String name) {
    super(name);
}
MyThread thread1 = new MyThread("A");

小练习-售票1

两个站点买票,各卖100张票,一共卖200张

public class Station extends Thread{
    private int ticket = 30;

    public Station() {
    }

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

    @Override
    public void run() {
        while (true) {
            if (ticket <= 0) {
                System.out.println(Thread.currentThread().getName() + " 票卖完了");
                break;
            }
            System.out.println(Thread.currentThread().getName() + " 卖出了第 " + (30 - ticket) + " 张票");
            ticket--;
        }
    }
}
public class StationTest {
    public static void main(String[] args) {
        Station station1 = new Station("窗口1");
        Station station2 = new Station("窗口2");
        station1.start();
        station2.start();
    }
}

内存图

每个线程都会拥有一个自己的栈
在这里插入图片描述

2、实现Runnable接口

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread1 = new Thread(myRunnable, "A");
        Thread thread2 = new Thread(myRunnable, "B");
        thread1.start();
        thread2.start();
    }
}

小练习-售票2

两个窗口,销售同一个站点的票,共卖30张

(目前没有保证安全性问题,主要用来理解内存图)

public class Station implements Runnable {
    private int ticket = 30;

    @Override
    public void run() {
        while (true) {
            if (ticket <= 0) {
                System.out.println(Thread.currentThread().getName() + " 票卖完了");
                break;
            }
            System.out.println(Thread.currentThread().getName() + " 卖出了第 " + (30 - ticket) + " 张票");
            ticket--;
        }
    }
}
public class StationTest {
    public static void main(String[] args) {
        Station station = new Station();
        Thread thread1 = new Thread(station, "窗口1");
        Thread thread2 = new Thread(station, "窗口2");
        thread1.start();
        thread2.start();
    }
}

查看源码

存储线程的名字

private volatile String name;

存储可执行的对象(此例子中存储的就是station对象)

private Runnable target;

构造方法:将两个参数传入,最后进行赋值

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}

内存图

每个线程都拥有一个自己的栈
在这里插入图片描述

3、Callable

带返回值的方式

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            private int count = 100;
            @Override
            public Integer call() throws Exception {
                count += 100;
                return count;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        Integer result = futureTask.get();
        System.out.println("结果: " + result);
    }
}

线程的常用方法

sleep()与wait()方法

另一种休眠方式

TimeUnit.SECONDS.sleep(1000);

sleep

进入休眠状态,休眠之后,不会抢占CPU,休眠结束后,进入就绪状态


wait

线程进入等待队列等待,释放CPU和锁,等待结束后,进入就绪状态


sleep与wait的区别

  • sleep是线程进入休眠状态,wait是线程进入等待队列
  • sleep会释放CPU,不会释放锁;wait会释放CPU和锁
  • sleep必须捕获异常,wait不用捕获异常

yield()方法

主动放弃CPU时间片,回到就绪状态,竞争下一次时间片

只把CPU使用权让给和自己权限相同,或者比自己权限更高的线程


join()方法

允许其它线程加入到当前线程中,并阻塞当前线程,等待加入线程执行完毕

(也就是阻塞,并等待加入线程执行完毕)

/**
 * @author 张宝旭
 */
public class SleepTest {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " 滴滴..." + i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        try {
            System.out.println("thread.join start......");
            thread.join();
            System.out.println("thread.join end......");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main end......");
    }
}

执行结果

thread.join start......
Thread-0 滴滴...0
Thread-0 滴滴...1
Thread-0 滴滴...2
Thread-0 滴滴...3
Thread-0 滴滴...4
thread.join end......
main end......

interrupt()方法

中断线程

thread.interrupt();

setDaemon()方法

设置为守护线程

thread.setDaemon(true);

线程安全

什么是线程安全

  • 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
    • 临界资源:共享资源,一次仅允许一个线程使用,才可保证其正确性
    • 原子操作:不可分割的多步操作,被视为一个整体,其顺序和步骤不可打乱缺省

synchronized

JDK1.6之后synchronized的优化:

  1. JDK1.6之前只有两种状态:无锁和重量级锁,JDK1.6之 后有四种状态:无锁、偏向锁、轻量级锁、重量级锁,无锁–>偏向锁–>轻量级锁–>重量级锁,只能升级不能降级。
  2. 优化策略:锁消除、锁粗化、自旋锁(jdk1.4包含) 、自适应自旋锁
    自旋锁是在轻量级锁升级为重量级时使用的优化策略。

同步代码块

synchronized (临界资源对象) {
    // 代码
}
  • 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
  • 线程退出同步代码块时,会释放相应的互斥锁标记。

售票例子

public class Station implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket <= 0) {
                    System.out.println(Thread.currentThread().getName() + " 票卖完了");
                    break;
                }
                System.out.println(Thread.currentThread().getName() + " 卖出了第 " + (100 - ticket) + " 张票");
                ticket--;
            }
        }
    }
}
public class StationTest {
    public static void main(String[] args) {
        Station station = new Station();
        Thread thread1 = new Thread(station, "窗口1");
        Thread thread2 = new Thread(station, "窗口2");
        Thread thread3 = new Thread(station, "窗口3");
        Thread thread4 = new Thread(station, "窗口4");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

执行结果:不同线程,按顺序卖出

窗口1 卖出了第 0 张票
窗口2 卖出了第 1 张票
窗口2 卖出了第 2 张票
窗口2 卖出了第 3 张票
...

同步方法

  • 非静态方法,锁住的是当前对象

  • 静态方法,锁住的是当前类

public synchronized void show() {
    for(int i = 0; i < 10; i++) {
        System.out.println(Thread.currentThread().getName() + "...");
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值