文章目录
一、进程(Process)与线程(Thread)
说的进程,先来提一下程序的概念:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态概念。
进程:进程是程序执行一次的过程,是一个动态概念,同时也是系统资源分配的基本单位。
线程:线程是一个独立执行的路径,它包含在进程之中,通常情况下一个进程可以包含若干个线程,我们熟知的main就是一个主线程,当然一个进程中至少有一个线程,不然它就没有存在的意义了,线程是CPU调度的基本单位。
再说说多线程,其实很多多线程都是模拟出来的,真正情况下的多线程是有多个CPU的,例如服务器等等,而 模拟出来的多线程,其实是在一个CPU的情况下,在同一时间点,CPU只能执行一个代码,但因为切换速度快,所以产生了同时执行的错觉。
相关知识:
1.程序运行时,即使自己没有创建线程,后台也会有多个线程,例如主线程main,垃圾回收线程gc等。
2.main()为主线程,是系统的入口,用于执行整个程序。
3.我们对同一份资源进行操作时,会存在资源抢夺问题,需要加入并发控制。
4.每个线程在自己的工作内存交互,内存控制不当会导致数据不一致。
二、线程的创建
创建方式:
继承Thread类
实现Runnable接口
实现Callable接口
1.继承Thread类
Thread实现了Runnable接口,要通过继承Thread类实现多线程,需要 分三步:
1.自定义线程类继承Thread类;
2.重写run()方法,编写线程执行体;
3.创建线程对象,调用start()方法启动线程。
代码示例:
//创建线程方式 继承Thread类
public class TestThread1 extends Thread {
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("看电视" + i);
}
}
public static void main(String[] args) {
//创建线程对象
TestThread1 t1 = new TestThread1();
//start方法开启线程
// t1.run();
t1.start();
//main线程
for (int i = 0; i < 500; i++) {
System.out.println("我在玩游戏" + i);
}
}
}
结果:
我们可以知道,当我们直接调用t1.run()时,run方法因为没有并发性,所以会先执行run方法再执行for循环,而当我们通过t1.start()启动线程时,就会出现上述图片结果,两个线程交替执行,一般来说是t1线程比主线程慢,因为start()启动线程需要时间。调用run和start的区别可以参考下图:
2.实现Runnable接口
该方法需要 分三步:
1.定义MyRunnable类实现Runnable接口;
2.实现run()方法,编写线程执行体;
3.创建线程对象,调用start()方法启动线程。
代码示例:
//创建线程方式2 实现接口
public class MyRunnable implements Runnable{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("看电视" + i);
}
}
public static void main(String[] args) {
//创建线程对象
MyRunnable t = new MyRunnable();
new Thread(t).start();
//main线程
for (int i = 0; i < 500; i++) {
System.out.println("我在玩游戏" + i);
}
}
}
二者比较:
继承Thread类:不建议使用,有单继承局限性,如果使用了就不能继承其他类了。
实现Runnabke类:推荐使用,避免了单继承局限性,灵活方便,方便同一个对象被多个线程使用。
一个对象被多个线程使用图示:
一个对象被多个线程使用代码示例:买火车票案例,该案例里面是有错误的,线程会抢夺相同的资源,大家运行一下就知道了,后续会说:
//多个线程同时操作一个对象
//买火车票例子
public class TestThread3 implements Runnable{
private int ticket = 20;
@Override
public void run() {
while (true){
if (ticket <= 0){
break;
}
//模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//Thread.currentThread().getName()获取当前线程的调用者
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticket-- + "张票");
}
}
public static void main(String[] args) {
TestThread3 t = new TestThread3();
new Thread(t,"小菜").start();
new Thread(t,"小许").start();
new Thread(t,"小困").start();
}
}
3.实现Callable接口
该方法(暂时了解即可,后续可能会补充)需要 步骤为:
1.实现Callable接口,需要返回值类型;
2.重写call方法,需要抛出异常;
3.创建目标对象;
4.创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
5.提交执行:Future result1 = ser.submit(t1);
6.获取结果:boolean r1 = result1.get();
7.关闭服务:ser.shutdownNow()。
三、线程的状态和分类
1.线程的状态
线程的五个状态及其转化图:
线程状态。 线程可以处于以下状态之一:
NEW :尚未启动的线程处于此状态。
RUNNABLE :在Java虚拟机中执行的线程处于此状态。
BLOCKED :被阻塞等待监视器锁定的线程处于此状态。
WAITING :正在等待另一个线程执行特定动作的线程处于此状态。
TIMED_WAITING :正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
TERMINATED :已退出的线程处于此状态。
PS:线程不能启动两次
2.线程的分类
线程可以分为 用户线程和守护线程,虚拟机必须保证用户线程执行完毕,不用等待守护线程执行完毕,守护线程诸如后台操作日志、监控内存、垃圾回收线程gc等等。
四、线程的方法
线程的常用方法:
1.线程的停止
代码示例:
//测试stop
//1.建议线程正常停止 利用次数 不建议死循环
//2.建议使用标志位 设置一个标志位
//不要使用stop或者destroy等过时或者JDK不建议使用的方法
public class TestStop implements Runnable {
//设置一个标志位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag) {
System.out.println("Thread : " + i++);
}
}
//设置一个公开的方法停止线程
public void stop() {
this.flag = false;
}
public static void main(String[] args) {
TestStop t = new TestStop();
new Thread(t).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main : " + i);
if (i == 900) {
t.stop();
System.out.println("线程该停止了!");
}
}
}
}
2.线程的休眠
使用sleep函数,该函数以ms为单位。同时需要知道:每个对象都有一把锁,sleep不会释放锁。
代码示例:
//模拟倒计时
public class TestSleep_1 {
public static void main(String[] args) {
tenDown();
}
public static void tenDown() {
int num = 10;
while (true) {
//延时1s 每秒输出一次
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
if (num == 0) {
break;
}
}
}
}
3.线程礼让
线程礼让使用yield方法,调用该方法时,让当前正在执行的线程暂停,但不进入阻塞状态,而是重新变为就绪状态,让CPU重新调度,所以说,线程礼让是不一定会成功的,得看CPU的心情,比如,你调用了yield,下次CPU依旧调度你,这是可能发生的。
4.线程插队
join方法可以使其他线程阻塞,优先执行本线程,本线程执行完在执行其他线程。
代码示例:
//测试join方法 类似插队
public class TestJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i < 500; i++) {
System.out.println("VIP线程--" + i);
}
}
public static void main(String[] args) throws InterruptedException {
//启动
TestJoin t = new TestJoin();
Thread thread = new Thread(t);
thread.start();
//主线程
for (int i = 0; i < 1000; i++) {
System.out.println("main线程--" + i);
if (i == 50) {
thread.join();
}
}
}
}
五、线程同步
1.线程同步机制、锁机制、死锁
线程同步机制:在多线程问题上,若多个线程访问同一个对象,并且有可能修改这个对象,这时候就需要线程的同步。线程同步其实就是一种等待机制,多个需要同时访问这个对象的线程,进入这个对象的等待池形成队列,等待前面的一个线程使用完毕,下一个线程在继续使用。
线程同步的形成条件需要队列+锁。
锁机制:由于同一个进程的多个线程访问同一个存储空间,带来方便的同时也带来了冲突,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他的线程必须等待,使用后释放锁机可。
死锁:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
2.同步方法和同步块
synchronized方法: public synchronized void method(int args){};
synchronized块: synchroized (ojb){};
区别:
同步方法默认用this或当前类class对象作为锁;同步代码块可以选择以什么来加锁,比同步方法要更细,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;不太理解往这走–synchronized同步方法与同步块
3.简单的买票案例
线程不安全:
//线程不安全的买票案例
public class BuyTickets {
public static void main(String[] args) {
BuyTicket b = new BuyTicket();
new Thread(b, "小菜").start();
new Thread(b, "小徐").start();
new Thread(b, "小困").start();
}
}
class BuyTicket implements Runnable {
private int tickets = 20;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
//模拟一下延时
Thread.sleep(100);
buyTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void buyTicket() throws InterruptedException {
if (tickets <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "获得了第" + tickets-- + "张票");
}
}
synchronized方法:
public class BuyTickets {
public static void main(String[] args) {
BuyTicket b = new BuyTicket();
new Thread(b, "小菜").start();
new Thread(b, "小徐").start();
new Thread(b, "小困").start();
}
}
class BuyTicket implements Runnable {
private int tickets = 20;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
//模拟一下延时
Thread.sleep(100);
buyTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void buyTicket() throws InterruptedException {
if (tickets <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "获得了第" + tickets-- + "张票");
}
}
synchronized同步代码块:
public class BuyTickets {
public static void main(String[] args) {
BuyTicket b = new BuyTicket();
new Thread(b, "小菜").start();
new Thread(b, "小徐").start();
new Thread(b, "小困").start();
}
}
class BuyTicket implements Runnable {
private int tickets = 20;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
//模拟一下延时
Thread.sleep(100);
buyTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void buyTicket() throws InterruptedException {
synchronized ((Object) tickets) {
if (tickets <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "获得了第" + tickets-- + "张票");
}
}
}
4.Lock锁
Lock是一个显示锁,需要手动开启手动关闭,不同于synchronized是一个隐式锁(出了作用域自动释放),Lock锁的性能高于synchronized锁,在使用时,选择优先级为:Lock > 同步代码块 > 同步方法。
使用格式:
//加锁
lock.lock();
try {
//需要保证线程安全的代码
}
} finally {
//解锁
lock.unlock();
}
买票案例改:
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
public static void main(String[] args) {
Buy b = new Buy();
new Thread(b, "小菜").start();
new Thread(b, "小徐").start();
new Thread(b, "小困").start();
}
}
class Buy implements Runnable {
private int tickets = 20;
//定义锁
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
if (tickets > 0) {
//模拟延时
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//加锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获得了第" + tickets-- + "张票");
} finally {
//解锁
lock.unlock();
}
} else {
break;
}
}
}
}
5.线程通信:生产者消费者问题
问题介绍 -> 生产者消费者问题
图示:
代码实现:
package Synchronized;
public class TestPC {
public static void main(String[] args) {
SynContainer c = new SynContainer();
new Producter(c).start();
new Consumer(c).start();
}
}
//缓冲区
class SynContainer {
private String[] s = new String[10];
//记录个数
private int idx = 0;
//供生产者调用
public synchronized void push(String str) {
//缓冲区满了 生产者等待
if (idx == s.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//没满
s[idx++] = str;
//通知消费者可以消费了
this.notify();
}
//供消费者调用
public synchronized String pop() {
//如果缓冲区没有产品 消费者等待
if (idx == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有产品
idx--;
String product = s[idx];
//通知生产者可以生产了
this.notify();
return product;
}
public String[] getS() {
return s;
}
}
//生产者
class Producter extends Thread {
private SynContainer container;
public Producter(SynContainer container) {
this.container = container;
}
public void run() {
for (int i = 0; i < container.getS().length; i++) {
String producter = "产品" + i;
container.push(producter);
System.out.println("生产了:" + producter);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
class Consumer extends Thread {
private SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
public void run() {
for (int i = 0; i < container.getS().length; i++) {
String consumer = container.pop();
System.out.println("消费了:" + consumer);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
六、总结
其实没啥总结,边听课边记录的,感觉深度应该是不太够的,应该只能做做基础,这块好像是面试的重点,后续应该还要重新深入学习。