1 多线程
程序:数据结构+算法,主要指存放在硬盘上的可执行文件
进程:运行在内存中的可执行文件
进程是重量级的,每新建一个进程会消耗CPU和内存等系统资源,因此进程的数量比较局限。为了解决这个问题,提出线程的概念,所谓线程就是进程内部的程序流,操作系统内部支持多进程,而进程的内部有支持多线程,线程事情量级的,线程不会在消耗系统资源,只会共享所在进程的系统资源,这正是多线程的优势所在,目前主流的开发都使用多线程
多线程采用时间片轮换法来保证多个线程的并发,所谓并发是指宏观上并行微观上依旧是串行的运行机制。
2 多线程的创建
Thread类代表线程,任何线程对象都是Thread类的实例,是线程的模版,封装了复杂的线程开启等操作,封装了操作系统的差异性,其常用方法:
| 常用方法 | 功能介绍 |
|---|---|
| Thread() | 无参方式构造对象 |
| Thread(Runnable target) | 根据参数引用构造对象 |
| Thread(Runnable target,String name) | 根据参数引用和名称构造对象 |
| Thread(String name) | 根据名称构造对象 |
| void run() | 若使用Runnable引用构造线程对象,调用该方法时最终调用接口中的版本 若没有使用Runnable引用构造线程对象,调用该方法时则不执行任何操作 |
| void start() | 启动线程,jvm虚拟机会自动调用线程的run方法 |
多线程的创建具体代码:
1 自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法
public class ThreadCreat extends Thread {
@Override
public void run() {
for (int i=1;i<10;i++){
System.out.println(i);
}
}
public static void main(String[] args) {
ThreadCreat threadCreat=new ThreadCreat();
threadCreat.start();
}
}
2 实现Runnable接口并重写run方法
public class ThreadCreat implements Runnable {
public static void main(String[] args) {
ThreadCreat threadCreat=new ThreadCreat();
Thread th=new Thread(threadCreat);
th.start();
}
@Override
public void run() {
for (int i=1;i<10;i++){
System.out.println(i);
}
}
}
3 匿名内部类的方式
public class ThreadCreat {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(i);
}
}
}.start();
}
}
从上面三种创建线程的方式可以看出:继承Thread类的方式代码简单,但是由于继承了Thread类,就不能在继承其他类了;而实现Runnable接口的方式代码复杂,但不会影响该类继续继承其他类或实现其他接口,因此更加推荐实现Runnable接口方式。
3 线程的执行流程以及生命周期
线程的执行流程是:
- 执行main方法的线程叫做主线程,执行run方法的线程叫做子线程。
- main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成功后线程的个数由1个变成2个,新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响
- 当run方法执行完毕后子线程结束,当mian方法结束后主线程结束
- 注意:两个线程执行没有明确的先后执行次序,由操作系统的调度算法决定
线程有五种状态,其生命周期如下:
-
新建状态:使用new关键字创建之后进入的状态,此时线程还未执行
-
就绪状态:调用start方法之后的状态,此时线程还未执行,在等待系统调度
-
运行状态:线程调度器调用该线程后的状态,这时线程开始运行,当线程的时间片执行完毕,但任务还没有完成,就进入就绪状态
-
消亡状态:当线程的任务执行完毕后就进入消亡状态,此时线程终止
-
阻塞状态:当线程执行的过程中发生阻塞事件,线程就会进入阻塞状态,当阻塞状态解除后,就进入就绪状态
4 多线程的相关属性和方法
线程是具有编号和名称的,通过下面的方法就可以得到多线程的相关属性:
| 方法声明 | 功能介绍 |
|---|---|
| long getId() | 返回线程的编号 |
| String getName() | 返回线程的名称 |
| void setName(String name) | 设置线程的名称为参数指定名称 |
| static Thread currentThread() | 获取当前正在执行的线程的引用 |
案例:自定义类继承Thread类并重写run方法,在run方法中先打印当前线程的名称和编号,然后将线程的名称修改为"张飞"后再次打印编号和名称(要求在main方法中也要打印主线程的编号和名称)
public class SubThread extends Thread {
@Override
public void run() {
Thread th=Thread.currentThread();
System.out.println("当前线程的编号是:"+th.getId());
System.out.println("当前线程的名称是:"+th.getName());
th.setName("zhangfei");
System.out.println("当前线程的编号是:"+th.getId());
System.out.println("当前线程的名称是:"+th.getName());
}
public static void main(String[] args) {
Thread th=Thread.currentThread();
System.out.println("主线程的编号是:"+th.getId());
System.out.println("主线程的名称是:"+th.getName());
SubThread st=new SubThread();
st.start();
}
}
Thread类常用方法:
| 方法声明 | 功能介绍 |
|---|---|
| static void yield() | 当前线程让出处理器,进入就绪状态等待 |
| static void sleep(int times) | 当前线程从运行态进入阻塞状态,休眠times毫秒,再返回到就绪态 |
| int getPriority() | 获取线程的优先级 |
| void setPriority(int priority) | 设置线程的优先级 |
| void join() | 等待该线程终止 |
| void join(long mills) | 等待参数指定的毫秒数 |
| boolean isDaemon() | 判断线程是否为守护线程 |
| void setDaemon(boolean on) | 设置线程为守护线程 |
案例:创建两个线程,线程一负责打印1~100之间的所有奇数,其中线程二负责打印1~100之间的所有偶数,在main方法启动上述两个线程同时执行,主线程等待两个线程终止
public class ThreadOne extends Thread{
@Override
public void run() {
for (int i=1;i<=100;i+=2){
System.out.println("子线程一中: i= "+i);
}
}
}
public class ThreadTwo extends Thread{
@Override
public void run() {
for(int i=2;i<=100;i+=2){
System.out.println("------子线程二中:i = "+i);
}
}
}
public class ThreatTest {
public static void main(String[] args) {
ThreadOne threadOne=new ThreadOne();
ThreadTwo threadTwo=new ThreadTwo();
threadOne.start();
threadTwo.start();
System.out.println("主线程开始等待....");
try {
threadOne.join();
threadTwo.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程等待结束");
}
}
5 线程同步机制
当多个线程同时访问一个共享资源时,会出现数据的覆盖不一致的问题,此时就需要对线程之间进行通信和协调,该机制叫做线程同步机制,并且多个线程并发读写同一个临界资源会发生线程并发安全问题
线程有同步操作和异步操作,其中同步操作是指多个线程串行运行,有先后的执行顺序;异步操作是指多个线程各自独立运行,没有先后顺序。
解决线程并发安全问题的方案:当一个线程执行完毕后,再让另一个线程执行,将线程的并发操作改为串行操作,这样就可以避免并发安全问题
具体实现方式:在java中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性,语法格式如下:
-
使用同步代码块的方式实现部分代码锁定,格式:
synchronized(类类型的引用){
编写所有需要锁定的代码;
}
-
使用同步方法的方式实现所有代码的锁定,格式:
直接使用synchronized关键字来修饰整个方法即可,等价于synchronized(this) {整个方法代码块};
静态方法的锁定:
-
对一个静态方法加锁:public synchronized static void xxx() {.....}
-
该方法的锁的对象是类对象,每个类只有一个类对象,获取类对象的方式:类名.class
-
静态方法与非静态方法同时使用synchronized后它们之间是非互斥关系,原因是:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象
使用synchronized保证线程同步的注意事项:
- 多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用
- 使用同步块时应尽量减少同步范围以提高并发的效率
使用线程同步机制的实例代码:
public class AcountRunnableTest implements Runnable {
private int balance;
private demon d=new demon();
public AcountRunnableTest() {
}
public AcountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"已启动");
synchronized (d) {
int temp = getBalance();
if (temp >= 200) {
System.out.println("正在出钞,请稍等");
temp -= 200;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走你的钞票");
} else {
System.out.println("余额不足,请换卡");
}
setBalance(temp);
}
}
public static void main(String[] args) {
AcountRunnableTest a1=new AcountRunnableTest(1000);
Thread t=new Thread(a1);
Thread t2=new Thread(a1);
t.start();
t2.start();
System.out.println("主线程开始等待");
try {
t.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终账户余额为:"+a1.getBalance());
}
}
class demon{}
6 线程安全类和不安全类、死锁的概念
线程安全类和不安全类:
- StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类
- Vector类和Hashtable类是线程安全的类,但ArrayList类和HashTable类不是线程安全的类
- Collections.synchronizedList()和Collections.synchronizedMap()等方法实现安全
死锁的概念:
线程一执行代码:
public void run(){
synchronized(a){ //持有对象锁a,等待对象锁b
synchronized(b){
编写锁定的代码;
}
}
}
线程二执行的代码:
public void run(){
synchronized(b){ //持有对象锁b,等待对象锁a
synchronized(a){
编写锁定的代码;
}
}
}
上面的代码就会造成死锁,线程一是持有对象锁a,但一直等待对象锁b,而线程二是持有对象锁b,一直等待对象锁a,这就会造成死锁,所以在开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用
7 Lock(锁)实现线程同步
java5开始提供了更强大的线程同步机制,即使用显示定义的同步锁对象来实现
Java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具,该接口主要实现类是ReentrantLock类,该类拥有与synchred相同的并发性,在以后的线程安全控制中,经常使用ReentrantLock类现实的加锁和释放锁
ReentrantLock类的常用方法:
| 方法声明 | 功能介绍 |
|---|---|
| ReentrantLock() | 无参方式构造对象 |
| void lock() | 获取锁 |
| void unlock() | 释放锁 |
实例代码:
import java.util.concurrent.locks.ReentrantLock;
public class AcountRunnableTest implements Runnable {
private int balance;
private demon d=new demon();
private ReentrantLock lock=new ReentrantLock();
public AcountRunnableTest() {
}
public AcountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
lock.lock();
System.out.println("线程"+Thread.currentThread().getName()+"已启动");
int temp = getBalance();
if (temp >= 200) {
System.out.println("正在出钞,请稍等");
temp -= 200;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走你的钞票");
} else {
System.out.println("余额不足,请换卡");
}
setBalance(temp);
lock.unlock();
}
public static void main(String[] args) {
AcountRunnableTest a1=new AcountRunnableTest(1000);
Thread t=new Thread(a1);
Thread t2=new Thread(a1);
t.start();
t2.start();
System.out.println("主线程开始等待");
try {
t.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终账户余额为:"+a1.getBalance());
}
}
class demon{}
与synchronized 方式的比较:
- Lock是显示锁,需要手动实现开启和关闭,而synchronized是隐式锁,执行锁定代码后自动释放
- Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁
- 使用Lock锁方式时,java虚拟机将花费较少的时间来调度性能,因此性能更好
8 Object类常用方法
Object类的常用方法介绍:
| 方法声明 | 功能介绍 |
|---|---|
| void wait() | 使线程进入等待态,直到其他线程唤醒 |
| void wait(long time) | 线程进入等待态,直到其他线程调用或参数指定的毫秒数过去 |
| void notify() | 唤醒单个线程 |
| void notifyAll() | 唤醒所有线程 |
实现两个线程之间的通信,具体实现代码如下:
public class ThreadCommunicate implements Runnable{
private int cnt;
@Override
public void run() {
while (true){
synchronized (this){
notify();
if(cnt<100){
System.out.println("线程"+Thread.currentThread().getName()+"中:cnt="+cnt);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
cnt++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
public static void main(String[] args) {
ThreadCommunicate tc=new ThreadCommunicate();
Thread thread=new Thread(tc);
thread.start();
Thread thread1=new Thread(tc);
thread1.start();
}
}
9 线程池
从java5开始增加创建线程的第三种方式是实现java.util.concurrent.Callable接口,接口常用方法:
| 方法声明 | 功能介绍 |
|---|---|
| V call() | 计算结果并返回 |
FutureTask类用于描述可取消的异步计算,该类提供了Future接口的基本实现,包括启动和取消计算、查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用后的返回结果,常用方法有:
| 方法声明 | 功能介绍 |
|---|---|
| FutureTask(Callable cal) | 根据参数引用创建一个未来任务 |
| V get() | 获取call方法计算的结果 |
线程池的概念:创建一些线程,它们的集合称为线程池,服务器收到客户的请求后,就从线程池中取出一个空闲的线程为它服务,服务后不关闭该线程,而是将线程返还给线程池
线程池的原理:线程池的编程模式下,任务是提交给线程池,而不是交给某个线程,线程池收到任务后,会为任务分配一个空闲的线程为之服务,注意任务是提交给整个线程池的,而不是某个线程,一个线程同时只能服务一个任务,但可以同时向一个线程池提交多个任务。
线程池实例代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadCallableTest implements Callable {
@Override
public Object call() throws Exception {
int sum=0;
for(int i=1;i<=10000;i++){
sum+=i;
}
System.out.println("计算结果是:"+sum);
return sum;
}
public static void main(String[] args) {
ThreadCallableTest tct=new ThreadCallableTest();
FutureTask ft=new FutureTask(tct);
Thread thread=new Thread(ft);
thread.start();
Object obj=null;
try {
obj=ft.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程处理结果的返回值是:"+obj);
}
}
线程池相关的类和接口是:java5开始提供线程池的类和接口是Executors类和ExecutorService接口,关于这两个类的常用方法如下
Executors类是工具类和线程池的工厂类,可以创建并返回不同类型线程池,常用方法如下:
| 方法声明 | 功能介绍 |
|---|---|
| static ExecutorService newCachedThreadPool() | 创建一个可根据需要创建新线程的线程池 |
| static ExecutorService newFixedThreadPool(int Threads) | 创建一个可重用固定线程数的线程池 |
| static ExecutorService newSingleThreadExecutor() | 创建一个只有一个线程的线程池 |
ExecutorService接口是真正的线程池接口,主要实现ThreadPoolExecutor,常用方法如下:
| 方法声明 | 功能介绍 |
|---|---|
| void execute(Runnable command) | 执行任务和命令,通常用于执行Runnable |
| Future submit(Callable task) | 执行任务和命令,通常用于执行Callable |
| void shutdown() | 启动有序关闭 |
10 生产者消费者模型的实现
实现生产者消费者模型,具体实现代码如下:
仓库代码
public class StoreHouse {
private int cnt=0;
public synchronized void product(){
notify();
if(cnt<10){
System.out.println("线程"+Thread.currentThread().getName()+"正在生产第"+(cnt+1)+"个产品");
cnt++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumer(){
notify();
if(cnt>1){
System.out.println("线程"+Thread.currentThread().getName()+"正在卖出第"+cnt+"个产品");
cnt--;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
生产者线程代码
public class ProductThread extends Thread{
private StoreHouse st;
public ProductThread(StoreHouse st) {
this.st = st;
}
@Override
public void run() {
while (true){
st.product();
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者代码
public class ConsumerThread extends Thread{
private StoreHouse st;
public ConsumerThread(StoreHouse st) {
this.st = st;
}
@Override
public void run() {
while (true){
st.consumer();
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试代码
public class StoreHouseTest {
public static void main(String[] args) {
StoreHouse st=new StoreHouse();
ProductThread pt=new ProductThread(st);
pt.start();
ConsumerThread cs=new ConsumerThread(st);
cs.start();
}
}