线程基础知识
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;
}
- 注: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的优化:
- JDK1.6之前只有两种状态:无锁和重量级锁,JDK1.6之 后有四种状态:无锁、偏向锁、轻量级锁、重量级锁,无锁–>偏向锁–>轻量级锁–>重量级锁,只能升级不能降级。
- 优化策略:锁消除、锁粗化、自旋锁(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() + "...");
}
}