Java学习记录(十九)多线程(一)

线程

线程是操作系统能进行调度的最小单位,他是被包含在进程中的,一个运行的软件可以看作为一个进程,而在该软件中执行的各种功能自身可以理解为一个线程,可以理解为在软件中互相独立又可以同时进行的功能,他是进程中实际的操作的单位

并发和并行

并发为在同一个cpu上同时多个指令交替进行(并发一个单位时间只处理一条指令,但是可以在这个单位时间内在多条指令之间跳转(也就是执行多个程序))

并行为在一个cpu上的多个线程或多个cpu上的多个线程中同时执行多个指令(并行一次处理多条指令)

开启线程

(一)第一种开启线程方法:继承Thread类

(该方法是在实现多线程的时候是在实现每个线程里的run方法,但run方法是没有返回值的,所以该种开启线程的方法适合在多线程执行任务时没有返回值的场景)

这种开启线程的步骤主要是使用strat方法去开启线程,首先需要一个需要开启线程的类去继承Thread类,并重写里面的run方法,run方法里面就是开启线程后需要执行的方法,再到其他类的main方法里面创建继承了Thread类该类的对象,通过该对象调用strat方法开启线程,具体如下:

package com.itazhang.Demo1;

public class MyTread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+"helloWorld");
        }
    }
}
package com.itazhang.Demo1;

public class TreadDemo1 {
    public static void main(String[] args) {
        MyTread mt = new MyTread();
        MyTread mt2 = new MyTread();
        mt.setName("线程1");
        mt2.setName("线程2");
        mt.start();
        mt2.start();
    }
}

(二)第二种开启线程的方法:实现Runnable接口

(该方法是在实现多线程的时候是在实现每个线程里的run方法,但run方法是没有返回值的,所以该种开启线程的方法适合在多线程执行任务时没有返回值的场景)

这种方法是需要开启线程的类实现该Runnable接口,重写里面的run方法,方法体里的内容就是该线程具体需要执行的内容,然后在其他类里面创建该类的对象(相当于创建一个任务对象),再创建Thread类的对象,在创建Thread类对象的时候将之前创建的对象传递给这个对象(相当于将之前创建的任务交给这个线程),最后再通过start方法开启线程。

实现如下:在Mythread类中,调用了Thread类里的currentThread方法,该方法主要是在执行该线程时返回该线程,再通过该线程调用getName方法从而在该线程执行的时候得到该线程的名字

package com.itazhang.Demo2;

public class MyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"helloworld");
        }
    }
}
package com.itazhang.Demo2;

import com.itazhang.Demo1.MyTread;

public class Test {
    public static void main(String[] args) {
        //创建MyThread对象,相当创建一个任务
        MyTread m1 =new MyTread();
        MyTread m2 =new MyTread();
        m1.setName("线程1");
        m2.setName("线程2");
        Thread t1 =new Thread(m1);
        Thread t2 =new Thread(m2);
        t1.start();
        t2.start();
    }
}

(三)第三种开启方式:实现Callable接口和Future接口的方式进行开启线程

在需要线程的返回值时采用该方法

这种方法是通过需要开启多线程的类去实现callable接口,callable接口会有一个泛型,该泛型就表示call方法里面也就是线程需要执行的方法在执行后返回的数据类型,例如我在执行了这个线程也就是执行了这个线程内的call方法后要返回一个字符串,那么这个泛型就是String。重写里面的call方法,方法内部就是这个线程需要执行的内容,到其他类中创建这个实现了Callable接口的类对象(这个对象相当于线程中需要执行的任务)

创建Future接口的实现类Futuretask的对象f,在创建时将之前的任务传递给他,这个时候就能通过该接口的实现类对象调用里面的get方法获取到执行该任务的线程的返回值

最后再创建一个Thread类的对象,在创建时将实现了Future接口的实现类对象f传递给该线程Thread对象。开启线程,使用f的get方法获取到该线程的结果

实现代码如下:

package com.itazhang.Demo3;

import java.util.concurrent.Callable;

public class MyThread implements Callable<String> {

    @Override
    public String call() throws Exception {
        String str = "000111";
        return str;
    }

}
package com.itazhang.Demo3;

import com.itazhang.Demo1.MyTread;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建任务
        MyThread m = new MyThread();
        //创建Future接口的实现类,通过该实现类的get方法得到上面实现该任务的线程的返回值
        FutureTask<String> f =new FutureTask<>(m);
        //创建线程,将该实现类对象交给该线程
        Thread t = new Thread(f);
        t.start();
        String s = f.get();
        System.out.println(s);

    }
}

线程内常用的成员方法 

前四种方法

在创建线程时不给线程设置名字,系统会自动的给线程设置默认名字,例如Thread0,如果使用继承Thread类的方式创建线程,且在创建该类对象的时候想直接传入线程的名字,那么在创建线程时需要重写该类的构造方法 ,如下:

package com.itazhang.Demo4;

public class MyThread extends Thread{
    public MyThread() {
        super();
    }

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

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("aaa000");
        }

    }
}

同时,如果需要在该线程执行一次之后睡眠一段时间就可以调用Thread的静态方法sleep方法,该方法内部的参数是long类型,表示睡眠n毫秒,例如传入1000就是执行一次睡眠1秒,如下:下列代码就会在该线程内执行一次for循环就会睡眠一秒,再执行一次for循环,直到run方法执行完

package com.itazhang.Demo4;

public class MyThread extends Thread{
    public MyThread() {
        super();
    }

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

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("aaa000");
        }

    }
}
package com.itazhang.Demo4;

import com.itazhang.Demo1.MyTread;

public class Test {
    public static void main(String[] args) {
        MyThread m = new MyThread("线程1");
        System.out.println(m.getName());
        m.start();
    }
}

线程的优先级

线程的优先级主要是表示的抢占到cpu的概率,线程的优先级大那么抢占到cpu的概率大,所以可以将一个线程的优先级设置大,这样该线程抢占cpu的概率就会变大

设置该线程的优先级主要是采用Thread类的setPriority,创建Thread相关对象调用该方法,传递进去1-10的数字,数字越大表明线程的优先级越高,抢占到cpu的概率越大,如果不设置优先级的话,线程默认的优先级数字是5。而获取到该线程的优先级,则是使用getpriority方法,实现代码如下:

package com.itazhang.Demo4;

import com.itazhang.Demo1.MyTread;

public class Test {
    public static void main(String[] args) {
        MyThread m = new MyThread("线程1");
        MyThread m1 = new MyThread("线程2");
        m.setPriority(1);
        m1.setPriority(10);
        m.start();
        m1.start();
    }
}

但是设置了线程的优先级只是增大了线程抢占cpu的概率,并不是一定能让该线程先抢占cpu执行完相关线程操作。

守护线程

守护线程是在其他非守护线程执行完毕后,就会自动缓缓的关闭该守护线程。也就是在其他非守护线程都执行完毕后,该守护线程也就没有存在的必要,会自动的慢慢关闭自己的线程。主要采用setDaemon方法将一个线程设置为守护线程,具体实现代码如下:

package com.itazhang.Demo4;

import com.itazhang.Demo1.MyTread;

public class Test {
    public static void main(String[] args) {
        MyThread m = new MyThread("线程1");
        MyThread m1 = new MyThread("线程2");
        m1.setDaemon(true);
        m.start();
        m1.start();
    }
}

插入线程

插入线程是将一个线程a调用join方法写入到另一个线程b中,这样的话线程b就会等待线程a执行完了之后才会执行,顾名思义相当于将线程a插入到了线程b的前面,具体实现代码如下:将m1插入到main方法中这时,main方法的线程会在m1线程执行完毕之后才会继续执行

package com.itazhang.Demo5;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThread m1 = new MyThread();
        m1.start();
        for (int i = 0; i < 10; i++) {
            m1.join();
            System.out.println("我是main方法执行的线程");
        }
    }
}

线程的生命周期

线程的生命周期如下图,在创建线程时,会处于新建状态,而调用start方法后会让该线程去抢占cpu,如果抢占到cpu就会开始执行相关的代码,在执行期间如果调用了sleep方法或者其他阻塞方法就会等待并重新抢夺cpu,在抢夺到cpu之后继续执行相关代码,如果执行完毕,最后就会线程死亡,变为垃圾线程。

线程安全问题以及解决方案

同步代码块

在实际情况中,如果多个线程共同执行一个任务的话,任务里的固定数据应该用static修饰,表示该数据是所有线程所共享的,而在实现过程中由于线程的抢占问题,所以可能会导致在执行run方法时,多个线程随机执行,例如一个卖票系统在卖票的时候,多个窗口表示多线程进行卖票操作,但是在卖票时由于多个线程出现抢夺问题,(每次进行卖票的时候获取票号,如果同时获取,即票号一样,多个窗口进行卖票)例如在1窗口准备将序号为2的票出售时,这时2窗口也是准备将序号为2的票出售,这时1窗口没有抢占到cpu资源于是无法此时将该序列号的票售出,但是2窗口抢到了cpu资源将这张票售出了,在2窗口售出之后,1窗口抢占到了cpu资源但是由于手里的票号没有售出所以票号没有改变,这个时候窗口1就会自动的将票号为2的票售出,这个时候就会导致每个窗口所卖的票可能一致,运行截图如下:

这个时候就需要一个名为锁的机制,例如窗口1在卖序列号2的票的时候将这个窗口锁起来,让顾客只能在这个窗口买票,买完了再将锁打开,让这几个窗口再抢夺资源,谁抢到了cpu资源再将他锁起来,让顾客只能在那里买票,如此重复,就能避免多个窗口卖出重复的票号问题

在代码中实现是由synchronized关键字来实现,使用了该修饰符的代码块是一次只能被一个线程访问,轮流执行,将需要进行锁的代码放入synchronized修饰的代码块当中即可达到锁的目的,这个关键字修饰的锁代码块需要传递一个锁对象,这个锁对象必须是唯一的,也就是静态的对象,用该对象作为锁对象,如下:static Object obj = new Object();这里的obj对象就是创建的静态锁对象,用以下代码实现方式就能解决多进程的安全问题

但是这个锁的对象必须是唯一的,也就是必须得是静态的,如果不是唯一的,相当于每个线程拥有自己的锁,将一个线程锁上之后,另外一个线程也能执行相关代码(可以理解为一个房间两个门分别带有两把锁,一个线程进入之后将锁锁上,但是另外一个线程能通过另一扇门的锁来进入该房间,这样的话锁就没有意义了,所以一般使用当前类的字节码对象当作锁对象)

package com.itazhang.Demo;

import java.util.concurrent.BlockingDeque;

public class MyThread extends Thread{
    static int ticket = 0;
//    //创建的静态锁对象
//    static Object obj = new Object();
    @Override
    public void run() {
        while(true){
            //使用锁
            synchronized (MyThread.class){
                if(ticket<100){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println("这是卖的第"+ticket+"张票");
                }else if(ticket>=100){
                    break;
                }
            }
        }

    }
}

同步方法

将同步代码块里面的方法抽取出来用synchronized修饰变成同步方法,他的作用与同步代码块一样

下面为一个实列代码(注意:下面创建的为实现runnable接口的线程开启方式,里面的任务也就是ticket不用设置成静态变量,因为在测试类里创建线程时会将该任务传递给多个线程,所以在多个线程中该任务本来就是共享的,所以不用设置为静态变量

写同步方法主要有如下步骤:

1、写相关的功能循环

2、写如果满足条件执行的功能

3、如果不满足,停止循环并执行的功能

4、将synchronizaed修饰代码块添加到循环当中,将循环里需要锁的代码放入该同步代码块

5、再将该同步代码块抽取成方法,并将方法里的同步代码块删除,将synchronized修饰符添加到方法中,让方法成为同步方法

package com.itazhang.Demo2;

public class MyThread1 implements Runnable{
    int ticket = 1;
    @Override
    public void run() {
        while(true){
            if (extracted()) break;
        }
    }

    private synchronized boolean extracted() {
            if(ticket<100){
                System.out.println("这时卖的第"+ticket+"张票");
                ticket++;
            }else if(ticket>=100){
                return true;
            }
        return false;
    }
}

  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是Java多线程编程学习笔记之十二:生产者—消费者模型的相关内容和代码。 ## 生产者—消费者模型简介 生产者—消费者模型是一种常见的多线程并发模型,它涉及到两个角色:生产者和消费者。生产者负责生产数据,消费者负责消费数据。生产者和消费者通过一个共享的缓冲区进行通信,生产者将数据放入缓冲区,消费者从缓冲区获取数据。 在多线程编程中,生产者—消费者模型的实现有多种方式,本文将介绍一种基于Java的实现方式。 ## 生产者—消费者模型的实现 ### 1. 定义共享缓冲区 共享缓冲区是生产者和消费者进行通信的桥梁,它需要实现以下功能: - 提供一个put方法,允许生产者将数据放入缓冲区; - 提供一个take方法,允许消费者从缓冲区获取数据; - 当缓冲区已满时,put方法应该等待; - 当缓冲区为空时,take方法应该等待。 以下是一个简单的共享缓冲区的实现: ```java public class Buffer { private int[] data; private int size; private int count; private int putIndex; private int takeIndex; public Buffer(int size) { this.data = new int[size]; this.size = size; this.count = 0; this.putIndex = 0; this.takeIndex = 0; } public synchronized void put(int value) throws InterruptedException { while (count == size) { wait(); } data[putIndex] = value; putIndex = (putIndex + 1) % size; count++; notifyAll(); } public synchronized int take() throws InterruptedException { while (count == 0) { wait(); } int value = data[takeIndex]; takeIndex = (takeIndex + 1) % size; count--; notifyAll(); return value; } } ``` 上面的Buffer类使用一个数组来表示缓冲区,size表示缓冲区的大小,count表示当前缓冲区中的元素数量,putIndex和takeIndex分别表示下一个可写和可读的位置。put和take方法都是同步方法,使用wait和notifyAll来进行线程间的等待和通知。 ### 2. 定义生产者和消费者 生产者和消费者都需要访问共享缓冲区,因此它们都需要接收一个Buffer对象作为参数。以下是生产者和消费者的简单实现: ```java public class Producer implements Runnable { private Buffer buffer; public Producer(Buffer buffer) { this.buffer = buffer; } public void run() { try { for (int i = 0; i < 10; i++) { buffer.put(i); System.out.println("Produced: " + i); Thread.sleep((int)(Math.random() * 1000)); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class Consumer implements Runnable { private Buffer buffer; public Consumer(Buffer buffer) { this.buffer = buffer; } public void run() { try { for (int i = 0; i < 10; i++) { int value = buffer.take(); System.out.println("Consumed: " + value); Thread.sleep((int)(Math.random() * 1000)); } } catch (InterruptedException e) { e.printStackTrace(); } } } ``` 生产者在一个循环中不断地向缓冲区中放入数据,消费者也在一个循环中不断地从缓冲区中获取数据。注意,当缓冲区已满时,生产者会进入等待状态;当缓冲区为空时,消费者会进入等待状态。 ### 3. 测试 最后,我们可以使用下面的代码来进行测试: ```java public class Main { public static void main(String[] args) { Buffer buffer = new Buffer(5); Producer producer = new Producer(buffer); Consumer consumer = new Consumer(buffer); Thread producerThread = new Thread(producer); Thread consumerThread = new Thread(consumer); producerThread.start(); consumerThread.start(); } } ``` 在上面的代码中,我们创建了一个缓冲区对象和一个生产者对象和一个消费者对象,然后将它们分别传递给两个线程,并启动这两个线程。 运行上面的代码,我们可以看到生产者和消费者交替地进行操作,生产者不断地向缓冲区中放入数据,消费者不断地从缓冲区中获取数据。如果缓冲区已满或者为空,生产者和消费者会进入等待状态,直到缓冲区中有足够的空间或者有新的数据可用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值