目录
一、创建线程的三种方式
1、继承Thread类
新建一个类,当这个类继承Thread类,同时重写Thread类中的run方法,并且线程的任务/逻辑写在run方法中,这个类才具备多线程能力。
注:(1)mian中创建具体的线程对象,想要执行线程中的任务,不能直接调用run方法,需要用Thread类中的start方法。
(2)线程对象创建并执行需要写在主线程欲执行代码之前。
代码示例:
以主函数和线程类分别需要打印十个数为例。
public class ThreadCreate extends Thread{
@Override
public void run() {
for(int i = 0; i < 10; i++){
System.out.println("Thread:"+i);
}
}
}
public class Test {
public static void main(String[] args) {
//创建一个具体的线程对象
ThreadCreate tc = new ThreadCreate();
//tc.run(); //不能直接调用run方法,直接调用会被当成是一个普通方法
tc.start(); //start()是Thread类中的方法
for(int i = 0; i < 10; i++)
System.out.println("main:"+i);
}
}
运行结果:

补充: 设置并读取线程名字
方式(1):setName(); getName();方法
主函数中使用Thread.currentThread().setName(“主线程”); 设置主函数的名字,其中Thread.currentThread():用于获取当前正在执行的线程
主函数中使用线程对象名.setName(“子线程”); 设置子线程名字,并在子线程中使用this.getName()获取当前子线程的名字。
代码示例:
Thread.currentThread().setName("主线程");
System.out.println(Thread.currentThread().getName()+i);
tc.setName("子线程");
System.out.println(this.getName()+i);
方式(2):通过构造器设置名字
子线程设置名字不使用setName();方法,转而在子线程中添加一个带参构造器。
public ThreadCreate(String name){
super(name);//调用父类的有参构造器
}
2、实现runnable接口
代码示例:
public class ThreadCreate2 implements Runnable{
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
public class Test {
public static void main(String[] args) {
//创建子线程对象
ThreadCreate2 tc2 = new ThreadCreate2();
Thread t1 = new Thread(tc2, "子线程1");
t1.start();
Thread t2 = new Thread(tc2, "子线程2");
t2.start();
Thread t3 = new Thread(tc2, "子线程3");
t3.start();
for(int i = 0; i < 100; i++)
System.out.println(Thread.currentThread().getName()+i);
}
注:
1、在实际开发中,使用方式2(实现Runnable接口)的方式更多
原因:(1)方式1会有Java单继承的局限性,如果继承了Thread类,就不能在继承其他的类
(2)方式2的共享资源的能力会更强
方式1在进行多线程共享资源时,可能会出现不同线程抢占同一资源,出现错误情况,即线程不安全,而方式2避免了这一情况。
2、Thread类实际也是Runnable接口的一个实现类
3、实现Callable接口
对比前两种创建线程的方式,都需使用run()方法。而run方法有局限性:没有返回值、不能抛出异常。为了弥补不足,提出第三种方式:实现Callable接口,但实现比较复杂。
1、实现Callable接口,可以不带泛型,如果不带泛型,默认返回object类型
2、如果带泛型,则返回泛型对应的类型
3、call方法有返回值,可以抛出异常
代码示例:
public class ThreadCreate3 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return new Random().nextInt(100);
}
}
class Test01{
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadCreate3 tc3 = new ThreadCreate3();
FutureTask ft = new FutureTask(tc3);
Thread t = new Thread(ft);
t.start();
Object obj = ft.get();
System.out.println(obj);
}
}
二、线程的生命周期
在线程的生命周期中,包括新建、就绪、运行、阻塞和消亡(死亡)五个状态。
新建状态: 当程序使用new创建一个线程,jvm为其分配内存并初始化,即为新建状态;
就绪状态: 当线程对象调用start()方法,就进入就绪状态,此时该线程有资格被运行,但还没获取到cpu的执行权;
运行状态: 处于就绪状态的线程获取cpu执行权,调用run()方法开始执行线程;
消亡状态: 当线程执行完毕,进入消亡状态。但有以下几种进入消亡状态的情况:(1)程序正常执行结束;(2)程序出现异常;(3)主动调用stop方法终止该线程(被弃用);
阻塞状态: 线程在运行状态出现阻塞事件会进入阻塞状态,当阻塞事件处理完毕,程序重新进入就绪状态。
图示:

三、线程常用方法
先前使用过的方法:
1、start():启动当前线程,表面上调用start方法,实际是调用线程里的run方法;
2、run():线程类继承Thread类或者实现Runnable接口的时候,都要重新实现这个run方法,run方法里面就是线程要执行的内容;
3、currentThread():Thread类中的一个静态方法:获取当前正在执行的线程;
4、setName():设置线程名字;
5、getName():读取线程名字;
新的方法:
1、设置优先级
对于同优先级别的线程,采取的策略是先到先服务,使用时间便轮转策略;对于不同优先级的,优先级别高的则被cpu调度的概率就大,但不代表一定优先调度。
方式:t.setPriority(10);
注意:必须先setPriority,再start!
2、join方法
当一个线程调用了join方法,这个线程会被先执行,它执行结束之后才可以执行别的线程。
方式:t.start();
t.join();
注意:必须先start,再join!
3、sleep方法
人为的制造阻塞事件
方式:Thread.sleep(1000);
4、setDaemon方法
设置伴随线程:将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不继续执行。
方式:t.setDaemon(true);
注意:必须先setDaemon,再join!
5、stop方法
四、线程同步
引入:多个线程,在争抢资源的过程中,导致共享的资源出现问题。一个线程还没执行完,另一个线程就参与进来,开始争抢资源。
解决方案:加“锁”(同步/同步监视器)
方式1、同步代码块
方式:synchronized (类.class){
…
}
注:必须保证多个线程用的是同一把锁!!!
代码示例(以抢票为例):
public class BuyTicketThread extends Thread{
public BuyTicketThread(String name){
super(name);
}
static int ticketNum = 10;
@Override
public void run() {
for(int i = 1; i <= 100; i++){
//把具有安全隐患的代码锁住即可,如果锁多了会降低效率
//this指的是这个锁
//synchronized (this){
//使用BuyTicketThread.class(当前类对应的字节码信息)当锁
synchronized (BuyTicketThread.class){
if(ticketNum > 0)
System.out.println("我在"+this.getName()+"买到了第"+ticketNum--+"张票");
}
}
}
}
public class BuyTicketMain {
public static void main(String[] args) {
BuyTicketThread btt1 = new BuyTicketThread("窗口1");
btt1.start();
BuyTicketThread btt2 = new BuyTicketThread("窗口2");
btt2.start();
BuyTicketThread btt3 = new BuyTicketThread("窗口3");
btt3.start();
}
}
同步监视器总结:
1、同步监视器必须是引用数据类型,不能是基本数据类型;
2、可以创建一个专门的同步监视器,没有任何业务含义;
3、一般使用共享资源做同步监视器即可;
4、在同步代码块中不能改变同步监视器对象的引用;
5、尽量不要使用String和Integer类做同步监视器;
6、建议使用final修饰同步监视器。
同步代码块的执行过程:
1、线程1来到同步代码块,发现同步监视器为open状态,需要close,然后执行其中的代码;
2、线程1在执行过程中,发生了线程切换(阻塞、就绪),线程1失去了cpu,但没有开锁open;
3、线程2获取了cpu,来到了同步代码块,发现同步监视器为close状态,无法执行其中的代码,线程2进入阻塞状态;
4、线程1再次获取cpu,接着执行后续代码,同步代码块执行完毕,释放锁open;
5、线程2也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复线程1的处理过程(加锁);
同步代码块中能发生cpu的切换,但后续被执行的线程也无法执行同步代码块!(因为锁仍处于close状态)
其他:
1、多个代码块使用同一个同步监视器(锁),锁住一个代码块的同时,也锁住使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块;
2、多个代码块使用同一个同步监视器(锁),锁住一个代码块的同时,也锁住使用该锁的所有代码块,但没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块。
方式2、同步方法
把可能会出现安全隐患的代码块封装成一个方法,该方法用static和synchronized修饰。
代码示例:
public class BuyTicketThread extends Thread{
public BuyTicketThread(String name){
super(name);
}
static int ticketNum = 10;
@Override
public void run() {
for(int i = 1; i <= 100; i++){
// synchronized (this){ }
buyTicket();
}
}
public static synchronized void buyTicket(){
if(ticketNum > 0)
System.out.println("我在"+Thread.currentThread().getName()+"买到了第"+ticketNum--+"张票");
}
}
public class BuyTicketMain {
public static void main(String[] args) {
BuyTicketThread btt1 = new BuyTicketThread("窗口1");
btt1.start();
BuyTicketThread btt2 = new BuyTicketThread("窗口2");
btt2.start();
BuyTicketThread btt3 = new BuyTicketThread("窗口3");
btt3.start();
}
}
总结:
1、不要将run()方法定义为同步方法,因为这样效率太低;
2、非静态同步方法的同步监视器是this,静态同步方法的同步监视器是类名.class 字节码信息对象;
3、同步代码块的效率要高于同步方法,因为同步方法是将线程挡在了方法的外部,而同步代码块将线程挡在了代码块的外部,但却是方法的内部;
4、同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法,同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块。
方式3、Lock锁
与synchronized相比,lock锁可提供多种锁方案,更灵活。
代码示例:
public class BuyTicketThread2 implements Runnable{
//创建一把锁
Lock lock = new ReentrantLock(); //多态(接口=实现类),可以使用不同的实现类
public static int ticketNum = 10;
@Override
public void run() {
for(int i = 1; i <= 100; i++){
//打开锁
lock.lock();
try {
if(ticketNum > 0)
System.out.println("我在"+Thread.currentThread().getName()+"买到了第"+ticketNum--+"张票");
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭锁
lock.unlock();
}
}
}
}
public class BuyTicketMain2 {
public static void main(String[] args) {
BuyTicketThread2 btt = new BuyTicketThread2();
Thread t1 = new Thread(btt, "窗口1");
Thread t2 = new Thread(btt, "窗口2");
Thread t3 = new Thread(btt, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的,是虚拟机级别的;而Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能更优越。
Lock和synchronized的区别:
1、Lock是显示锁(手动开启和关闭锁),synchronized是隐式锁;
2、Lock只有代码块锁,synchronized有代码块锁和方法锁;
3、使用Lock锁,JVM将话费较少的时间来调度线程,性能更好,并有更好的扩展性(提供更多的子类)。
优先使用顺序
Lock > 同步代码块(进入方法体,分配了相应资源) > 同步方法(方法体之外)
4、线程同步的优缺点
线程安全:效率低,可能带来死锁
线程不安全:效率高
五、线程通信
以生产者-消费者为例,假设只有一个空间,生产者生产完一个商品,消费者才能获取该商品,消费者消费完,生产者再生产,如此循环。
1、未加线程同步之前
代码示例:
Product(商品类):
public class Product {
private String brand;
private String name;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
ProducerThread(生产者线程):
public class ProducerThread extends Thread{
private Product p;
public ProducerThread(Product p) {
this.p = p;
}
@Override
public void run() {
for(int i = 1; i <= 10; i++){
if(i % 2 ==0){
p.setBrand("费列罗");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("巧克力");
}else {
p.setBrand("苹果");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("电脑");
}
System.out.println("生产者生产了"+p.getBrand()+"的"+p.getName());
}
}
}
ConsumerThread(消费者线程):
public class ConsumerThread extends Thread{
private Product p;
public ConsumerThread(Product p) {
this.p = p;
}
@Override
public void run() {
for(int i = 1; i <= 10; i++){
System.out.println("消费者消费了"+p.getBrand()+"的"+p.getName());
}
}
}
Main函数:
public class MainTest {
public static void main(String[] args) {
Product p = new Product();
ProducerThread pt = new ProducerThread(p);
ConsumerThread ct = new ConsumerThread(p);
pt.start();
ct.start();
}
}
未加线程同步之前,两个线程会出现争抢资源的情况,导致结果出现混乱。如下:

2、加线程同步后
为了解决上述问题,需要采用线程同步。
(1)同步代码块的方式
修改后的ProducerThread(生产者线程)部分代码:
@Override
public void run() {
for(int i = 1; i <= 10; i++) {
synchronized (p) {
if (i % 2 == 0) {
p.setBrand("费列罗");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("巧克力");
} else {
p.setBrand("苹果");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("电脑");
}
System.out.println("生产者生产了" + p.getBrand() + "的" + p.getName());
}
}
}
修改后的ConsumerThread(消费者线程)部分代码:
@Override
public void run() {
for(int i = 1; i <= 10; i++){
synchronized (p){
System.out.println("消费者消费了"+p.getBrand()+"的"+p.getName());
}
}
}
(2)同步方法的方式
修改后的Product(商品类)部分代码:
public synchronized void setProduct(String brand, String name){
this.setBrand(brand);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了" + this.getBrand() + "的" + this.getName());
}
public synchronized void getProduct(){
System.out.println("消费者消费了"+this.getBrand()+"的"+this.getName());
}
修改后的ProducerThread(生产者线程)部分代码:
@Override
public void run() {
for(int i = 1; i <= 10; i++) {
if (i % 2 == 0) {
p.setProduct("费列罗", "巧克力");
} else {
p.setProduct("苹果", "电脑");
}
}
}
修改后的ConsumerThread(消费者线程)部分代码:
@Override
public void run() {
for(int i = 1; i <= 10; i++){
p.getProduct();
}
}
3、wait()和notify()方法
上述代码可以解决线程同步问题,但会有生产者生产多个商品,消费者消费多个商品的情况,为了解决这一问题,使得生产者和消费者交替执行,引入wait()和notify()方法。
方法:
给product类加一个布尔类型的变量flag,用来标记是否有商品(true表示有商品,false表示没有商品);
分别在生产者和消费者同步方法中进行判断,决定是否生产/消费,是否wait()或nofity()。
修改后的Product(商品类)部分代码:
//true:有商品;false:没有商品
boolean flag = false;//默认情况下,没有商品,先生产再消费
public synchronized void setProduct(String brand, String name){
if(flag == true){ //如果为true,表示有商品,则生产者不生产,等待消费者消费
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.setBrand(brand);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了" + this.getBrand() + "的" + this.getName());
//生产完,flag变成true,同时通知消费者消费
flag = true;
notify();
}
public synchronized void getProduct(){
if(flag == false){ //如果为false,表示没有商品,消费者不消费,等待生产者生产
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者消费了"+this.getBrand()+"的"+this.getName());
//消费完,flag变成false,同时通知生产者生产
flag = false;
notify();
}
原理:在Java对象中,有两种池:锁池(synchronized)、等待池(wait(); notify(); notifyAll();)
如果一个线程调用了某个对象的wait方法,那么该线程进入到该对象的等待池中(并且已经将锁释放);如果未来的某一时刻,另一个线程调用了相同对象的notify方法或者notifyAll方法,那么该等待池中的线程就会被唤醒,然乎进入到对象的锁池里面去获得该对象的锁;如果获得锁成功,那么该线程就会沿着wait方法之后的路径继续执行,注意是沿着wait方法之后。(在代码中的体现就是只有if判断,没有else)
注意:
1、wait和notify方法必须放在同步方法或者同步代码块中才能生效(因为线程的通信只有在同步的基础上才有效);
2、sleep方法和wait方法的区别:sleep方法进入阻塞状态但没有释放锁,而wait进入阻塞状态同时释放了锁。
4、进一步优化
上述代码解决生产者-消费者问题,仍存在问题,即当有多个生产者,多个消费者时,一个生产者/消费者线程使用wait方法,其余的线程均进入等待队列,使用notify唤醒时,所有均被唤醒,这时生产者线程仍可能唤醒生产者线程,消费者线程仍可能唤醒消费者线程。出现这种问题,需要使用Lock锁和Condition进行解决。
修改后的Product(商品类)部分代码:
//Lock锁
Lock lock = new ReentrantLock();
//生产者等待队列
Condition produceCondition = lock.newCondition();
//消费者等待队列
Condition consumeCondition = lock.newCondition();
public void setProduct(String brand, String name){
lock.lock();
try {
if(flag == true){ //如果为true,表示有商品,则生产者不生产,等待消费者消费
try {
//wait();
produceCondition.await(); //生产者阻塞,生产者进入等待等列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.setBrand(brand);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
System.out.println(Thread.currentThread().getName()+":生产者生产了" + this.getBrand() + "的" + this.getName());
//生产完,flag变成true,同时通知消费者消费
flag = true;
//notify();
consumeCondition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void getProduct(){
lock.lock();
try {
if(flag == false){ //如果为false,表示没有商品,消费者不消费,等待生产者生产
try {
//wait();
consumeCondition.await(); //消费者阻塞,消费者进入等待队列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":消费者消费了"+this.getBrand()+"的"+this.getName());
//消费完,flag变成false,同时通知生产者生产
flag = false;
//notify();
produceCondition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
Main函数:
public class MainTest {
public static void main(String[] args) {
Product p = new Product();
ProducerThread pt = new ProducerThread(p);
ConsumerThread ct = new ConsumerThread(p);
Thread pt1 = new Thread(pt, "生产者线程1");
Thread pt2 = new Thread(pt, "生产者线程2");
Thread pt3 = new Thread(pt, "生产者线程3");
Thread ct1 = new Thread(ct, "消费者线程1");
Thread ct2 = new Thread(ct, "消费者线程2");
Thread ct3 = new Thread(ct, "消费者线程3");
pt1.start();
pt2.start();
pt3.start();
ct1.start();
ct2.start();
ct3.start();
}
}
运行结果:

Condition:
一种用来替代传统的Object的wait()、notify()实现线程间协作的方式
一个Condition包含一个等待队列,一个Lock可以产生多个Condition,所以可以有多个等待队列;
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而Lock(同步器)拥有一个同步队列和多个等待队列;
Object中的wait()、notify()、notifyAll()方法是和“同步锁”(synchronized关键字)捆绑使用,而Condition是与“互斥锁/共享锁”捆绑使用,就是说必须在lock.lock()和lock.unlock()之间才能使用。
本文详细介绍了Java中创建线程的三种方式:继承Thread类、实现Runnable接口和实现Callable接口,并探讨了线程的生命周期,包括新建、就绪、运行、阻塞和消亡状态。此外,还讲解了线程的常用方法,如设置优先级、join、sleep和设置守护线程。线程同步方面,通过同步代码块、同步方法和Lock锁进行了深入分析,同时讨论了线程同步的优缺点。最后,通过生产者-消费者模型展示了线程通信的wait、notify方法以及Lock和Condition的使用,强调了线程安全和效率的重要性。

981

被折叠的 条评论
为什么被折叠?



