JAVA多线程
文章目录
一、线程

1.进程

计算机的核心是CPU,它承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,
统领整个计算机硬件;应用程序是具有某种功能的程序,程序是运行于操作系统之上的。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用
程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序,数据集合和进程控制块三部分组成。程序用于描述
进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块包含进程的描述信息和控制信息
是进程存在的唯一标志
进程具有的特征:
动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
并发性:任何进程都可以同其他进行一起并发执行;
独立性:进程是系统进行资源分配和调度的一个独立单位;
结构性:进程由程序,数据和进程控制块三部分组成
2.线程

在早期的操作系统中并没有线程的概念,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片
轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明
了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或
多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组
成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。
进程与线程的区别
-
线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
-
一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
-
进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信
号等),某进程内的线程在其他进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多
进程是资源分配的最小单位,线程是CPU调度的最小单位
3.多线程的实现方案
1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
3.通过Callable和FutureTask创建线程
4.通过线程池创建线程
前面两种可以归结为一类:无返回值,原因很简单,通过重写run方法,run方式的返回值是void,所以没有办法返回结果。
后面两种可以归结成一类:有返回值,通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中。
继承Thread类,重写该类的run()方法
承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。
package Demo01;
//1、创建thread子类
public class MyThread extends Thread{
// 2、才重写run方法
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("子线程"+i);
}
}
}
package Demo01;
/*
* java.long.Thread
* 实现步骤
* 1、创建一个Thread类的子类
* 2、在Thread子类中重写run方法,设置线程任务(干什么)
* 3、创建Thread子类的对象
* 4、调用Thread类中的方法start来启动线程,执行run方法
*/
public class Demo01Thread {
public static void main(String[] args) {
// 创建thread子类的对象
MyThread mt = new MyThread();
// 4、调用start方法
mt.start();
for (int i = 0; i < 20; i++) {
System.out.println("主线程"+i);
// if (i==10) {
// System.out.println(0/0);
// }
}
}
}

实现Runnable接口,并重写该接口的run()方法
创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
package Demo04;
/*
* 实现runnable 接口创建多线程的好处
* 1、避免了单继承的局限性
* 2、增强了程序的扩展性,降低了程序的耦合性
*/
public class Demo04RTunnable {
public static void main(String[] args) {
//创建一个runnable接口的实现类对象
// RunnableImpl run =new RunnableImpl();
//创建Thread类对象,构造方法中传递Runnable 接口实现类对象
// Thread t = new Thread(run);
//调用Thread类中的start方法,启动子线程
Thread t = new Thread(new RunnableImpl2());
t.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}

package Demo04;
//1、创建一个runnable
public class RunnableImpl implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
package Demo04;
public class RunnableImpl2 implements Runnable{
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 20; i++) {
System.out.println("helloworld-->"+i);
}
}
}

使用Callable和Future接口创建线程
a:创建Callable接口的实现类 ,并实现Call方法
b:创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值
c:使用FutureTask对象作为Thread对象的target创建并启动线程
d:调用FutureTask对象的get()来获取子线程执行结束的返回值
public class ThreadTest {
public static void main(String[] args) {
Callable<Integer> myCallable = new MyCallable(); // 创建MyCallable对象
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程
thread.start(); //线程进入到就绪状态
}
}
System.out.println("主线程for循环执行完毕..");
try {
int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<Integer> {
private int i = 0;
// 与run()方法不同的是,call()方法具有返回值
@Override
public Integer call() {
int sum = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
return sum;
}
}
通过线程池创建线程
public class ThreadDemo05{
private static int POOL_NUM = 10; //线程池数量
/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i = 0; i<POOL_NUM; i++) {
RunnableThread thread = new RunnableThread();
//Thread.sleep(1000);
executorService.execute(thread);
}
//关闭线程池
executorService.shutdown();
}
}
class RunnableThread implements Runnable {
@Override
public void run() {
System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");
}
}
ExecutorService、Callable都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,还有Future接口也是属于这个框架,有了这种特征得到返回值就很方便了。
通过分析可以知道,他同样也是实现了Callable接口,实现了Call方法,所以有返回值。这也就是正好符合了前面所说的两种分类
执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
再介绍Executors类:提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
3.设置和获取线程名称
获取线程名称getName()方法
package Demo01;
public class Demo01MAainThread {
public static void main(String[] args) {
Person p1 =new Person("小强");
p1.run();
// System.out.println(0/0);
Person p2 =new Person("小刚");
p2.run();
}
}
package Demo01;
//1、创建thread子类
public class MyThread extends Thread{
// 2、才重写run方法
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("子线程"+i);
}
}
}

package Demo02;
/*
* java.long.Thread
* 实现步骤
* 1、创建一个Thread类的子类
* 2、在Thread子类中重写run方法,设置线程任务(干什么)
* 3、创建Thread子类的对象
* 4、调用Thread类中的方法start来启动线程,执行run方法
*/
public class Demo01Thread {
public static void main(String[] args) {
// 创建thread子类的对象
MyThread mt = new MyThread();
// 4、调用start方法
mt.start(); //子线程0
new MyThread().start();//子线程1
new MyThread().start();//子线程2
new MyThread().start();//子线程3
System.out.println("main"+Thread.currentThread().getName());
}
}
package Demo02;
/*
* java.long.Thread
* 实现步骤
* 1、创建一个Thread类的子类
* 2、在Thread子类中重写run方法,设置线程任务(干什么)
* 3、创建Thread子类的对象
* 4、调用Thread类中的方法start来启动线程,执行run方法
*/
public class Demo01Thread {
public static void main(String[] args) {
// 创建thread子类的对象
MyThread mt = new MyThread();
// 4、调用start方法
mt.start(); //子线程0
new MyThread().start();//子线程1
new MyThread().start();//子线程2
new MyThread().start();//子线程3
System.out.println("main"+Thread.currentThread().getName());
}
}
通过构造方法给线程设置名称
package Demo02;
/*
* java.long.Thread
* 实现步骤
* 1、创建一个Thread类的子类
* 2、在Thread子类中重写run方法,设置线程任务(干什么)
* 3、创建Thread子类的对象
* 4、调用Thread类中的方法start来启动线程,执行run方法
*/
public class Demo01Thread {
public static void main(String[] args) {
// 创建thread子类的对象
MyThread mt = new MyThread();
// 4、调用start方法
mt.start(); //子线程0
new MyThread().start();//子线程1
new MyThread().start();//子线程2
new MyThread().start();//子线程3
System.out.println("main"+Thread.currentThread().getName());
}
}
package Demo02;
//1、创建thread子类
public class MyThread extends Thread{
// 2、才重写run方法
public void run() {
String name = getName();
System.out.println("run:"+name);
System.out.println("子"+Thread.currentThread().getName());
}
}

通过setName()方法设置线程名称
package Demo02;
public class Demo02ThreeadSetName {
public static void main(String[] args) {
MyThreadName mt = new MyThreadName("小强");
mt.start();
new MyThreadName("旺财").start();
}
}
package Demo02;
public class MyThreadName extends Thread{
public MyThreadName() {}
public MyThreadName(String name) {
super(name);//把线程的名字传递给父类,让父类Thread给子线程取一个名字
}
public void run() {
String name = getName();
System.out.println("run:"+name);
System.out.println("子"+Thread.currentThread().getName());
}
}

4线程控制

5线程生命周期

package Demo03;
public class Demo03Sleep {
public static void main(String[] args) {
for (int i = 0; i < 60; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

二、线程同步
互斥锁(mutex)
互斥锁 是最常见的线程同步方式,它是一种特殊的变量,它有 lock 和 unlock 两种状态,一旦获取,就会上锁,且只能由该线程解锁,期间,其他线程无法获取
优点:
使用简单;
缺点:
重复锁定和解锁,每次都会检查共享数据结构,浪费时间和资源;
繁忙查询的效率非常低;
条件变量(condition)
针对互斥锁浪费资源且效率低的缺点,可以使用条件变量。
条件变量的方法是,当线程在等待某些满足条件时使线程进入睡眠状态,一旦条件满足,就唤醒,这样不会占用宝贵的互斥对象锁,实现高效
条件变量允许线程阻塞并等待另一个线程发送信号,一般和互斥锁一起使用。
条件变量被用来阻塞一个线程,当条件不满足时,线程会解开互斥锁,并等待条件发生变化。一旦其他线程改变了条件变量,将通知相应的阻塞线程,这些线程重新锁定互斥锁,然后执行后续代码,最后再解开互斥锁。
读写锁(reader-writer lock)
读写锁 也称之为 共享-独占锁,一般用在读和写的次数有很大不同的场合。即对某些资源的访问会出现两种情况,一种是访问的排他性,需要独占,称之为写操作;还有就是访问可以共享,称之为读操作。
读写所 相比于不管三七二十一,通通独占的模式,有着很大的适用性和并行性。其有以下几种状态:
读写锁处于写锁定的状态,则在解锁之前,所有试图加锁的线程都会阻塞;
读写锁处于读锁定的状态,则所有试图以读模式加锁的线程都可得到访问权,但是以写模式加锁的线程则会阻塞;
读写锁处于读模式的锁(未加锁)状态时,有另外的线程试图以写模式加锁,则读写锁会阻塞读模式加锁的请求,这样避免了读模式锁长期占用,导致的写模式锁长期阻塞的情况;
适用场景:
读写锁最适用于对数据结构的毒操作次数多于写操作次数的场合。
处理这种问题一般有两种常见的策略:
强读者同步
总是给读者更高的优先权,只要没有写操作,读者就可以获取访问权,比如图书馆查询系统采用强读者同步策略;
强写者同步
写者有更高的优先级,读者只能等到写者结束之后才能执行,比如航班订票系统,要求看到最新的信息记录,会使用强写者同步策略;
信号量(semphore)
信号量 和互斥锁的区别在于:互斥锁只允许一个线程进入临界区,信号量允许多个线程同时进入临界区
可以这样理解,互斥锁使用对同一个资源的互斥的方式达到线程同步的目的,信号量可以同步多个资源以达到线程同步

package Demo05;
public class Demo01Ticket {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}
package Demo05;
/*
* 解决线程安全问题的一种方案:使用同步代码块
* 格式:
* synchronized(锁对象){
* 可能出现线程安全的代码(是因为访问了共享的数据)
* }
* 注意:
* 1、通过代码块中的锁对象,可以使用任意的对象
* 2、但是必须保证多个线程使用的锁对象是同一个
* 3、锁对象作用:
* 把同步代码块锁住,只让一个线程执行
* */
public class RunnableImpl implements Runnable{
public int ticket = 100;
Object obj = new Object();
public void run() {
while (true) {
synchronized (obj) {
if (ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
}

10万+

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



