一、进程与线程
1、进程(process)
每个独立运行着的程序称为一个进程,它是操作系统分配资源的基本单位,有独立的内存空间和系统资源。
2、线程(thread)
线程是进程内部的一条执行路径(path),Java虚拟机允许应用程序并发的运行多个执行路径,它是进程中执行运算的最小单位,处理机分配给线程,即真正在处理机上运行的是线程。
3、进程和线程区别
进程有独立的地址空间,一个进程崩溃后,不会对其它进程产生影响,而线程只是一个进程中的一个执行路径,如有一条线程崩溃了,可能会影响同进程中的其他的线程。
线程有自己的栈和局部变量,多个线程共享同一进程的地址空间
一个进程至少有一个线程
4、线程的生命周期
线程的状态:
要想实现多线程,必须在主线程中创建新的线程对象。任何线程都具有五种状态:创建、就绪、运行、阻塞、终止。
5、线程的停止
如果线程的run()方法中执行的是一个重复执行的循环,可以提供一个标记来控制循环是否执行。
如果线程因为执行sleep()或是wait()而进入了阻塞状态,此时要想停止它,可以使用interrupt(),程序会抛出InterruptException异常。
如果程序因为输入/输出的等待而阻塞,基本上必须等待输入/输出的动作完成才能离开阻塞状态。无法用interrupt()方法来使得线程离开run()方法,要想离开,只能通过引发一个异常。
二、多线程的提出
多线程就是在一个进程中创建多个线程,每个线程完成一个任务
â优点
● 多线程技术使程序的响应速度更快
● 提高资源利用率
● 程序设计更简单
â多线程执行特性
随机性(异步执行)谁”抢”到cpu,谁执行宏观上同时执行,微观上同一时刻只能执行一个线程,多核除外
三、线程的创建与启动
1、创建线程的第一种方式:继承Thread类。
第一种方式:将类声明为Thread 的子类并重写run()方法
class MyThread extends Thread{
public void run(){
线程具体执行的代码
}
}
创建此线程类的实例并启动:
MyThread thread1 = new MyThread();
thread1.start(); // 启动线程
实例
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo();
Thread t1 = new Thread(d);//创建线程
Thread t2 = new Thread(d);
t1.start();//启动线程
t2.start();
}
}
1、创建线程的第二种方式:实现Runnable接口。
1,定义类实现Runnable接口。
2,覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3,通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。
所以要在线程对象创建时就必须明确要运行的任务。
4,调用线程对象的start方法开启线程。
实现Runnable接口的好处:
1,将线程的任务从线程的子类中分离出来,进行了单独的封装。
按照面向对象的思想将任务的封装成对象。
2,避免了java单继承的局限性。
所以,创建线程的第二种方式较为常用。
定义实现Runnable接口的类
Runnable接口中只有一个方法 public void run(); 用来定义线程运行体:
class MyRun implements Runnable{
public void run(){
线程执行的具体代码
}
}
创建线程的实例的时候将这个类的实例作为参数传递到线程实例内部。然后再启动:
Thread thread1 = new Thread(new MyRun());
thread1.start();
实例:
class Demo implements Runnable//extends Fu //准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行。
//通过接口的形式完成。
{
public void run()
{
show();
}
public void show()
{
for(int x=0; x<20; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
}
}
class Thread
{
private Runnable r;
Thread()
{
}
Thread(Runnable r)
{
this.r = r;
}
public void run()
{
if(r!=null)
r.run();
}
public void start()
{
run();
}
}
class ThreadImpl implements Runnable
{
public void run()
{
System.out.println("runnable run");
}
}
ThreadImpl i = new ThreadImpl();
Thread t = new Thread(i);
t.start();
class SubThread extends Thread
{
public void run()
{
System.out.println("hahah");
}
}
1、通过 Callable接口实现多线程
1. Callable接口介绍
java.util.concurrent.Callable是一个泛型接口,只有一个call()方法
call()方法抛出Exception异常,且返回一个指定的泛型类的对象
2. Callable接口实现线程的应用场景
当父线程想要获取子线程的运行结果时
3.使用Callable接口实现多线程的步骤
第一步:创建Callable子类的实例化对象。
第二步:创建FutureTask 对象,并将Callable对象传入FutureTask的构造方法中(注意:FutureTask实现了Runnable接口和Future接口)
第三步:实例化Thread对象,并在构造方法中传入FutureTask对象
第四步:启动线程
Object中的几个方法支持
wait() 线程等待,当前线程进入调用对象的线程 等待池
notify() 唤醒1个等待线程
notifyAll() 唤醒全部等待的线程
注意:以上三个方法都必须在同步机制中调用
代码示例:
public class AnonyCallable {
public static void main(String[] args) {
Callable<String> call=new Callable<String>(){
@Override
public String call() throws Exception {
String threadName=Thread.currentThread().getName();
System.out.println(threadName+"开始偷袭...");
System.out.println(threadName+"不幸遇到大股敌军");
return "偷袭失败,只剩我一个人了!";
}
};
FutureTask<String> task=new FutureTask<String>(call);
new Thread(task,"魏延线程").start();
System.out.println(Thread.currentThread().getName()+"进入休眠状态(休眠5秒)...");
try {
Thread.sleep(5000);
String report=task.get(); // 获取子线程的返回值
System.out.println("子线程的返回结果是:"+report);
} catch (Exception e) {
e.printStackTrace();
}
}
}
package callable;
import java.util.concurrent.FutureTask;
public class CallableDemo {
public static void main(String[] args) {
GeneralCallable call=new GeneralCallable(); // 创建Callable子类的实例化对象
FutureTask<String> task=new FutureTask<String>(call); // 创建FutureTask 对象,并将Callable对象传入FutureTask的构造方法中
Thread t=new Thread(task,"魏延线程"); // 实例化Thread对象,并在构造方法中传入FutureTask对象
t.start(); // 启动线程
System.out.println(Thread.currentThread().getName()+"进入休眠状态(休眠5秒)...");
try {
Thread.sleep(5000);
String report=task.get(); // 获取子线程的返回值
System.out.println("子线程的返回结果是:"+report);
} catch (Exception e) {
e.printStackTrace();
}
}
}
package callable;
import java.util.concurrent.Callable;
public class GeneralCallable implements Callable<String>{
@Override
public String call() throws Exception {
String threadName=Thread.currentThread().getName();
System.out.println(threadName+"开始偷袭...");
System.out.println(threadName+"与小股敌人作战");
return "偷袭成功!";
}
}
1、多线程安全与同步问题
当run()方法体内的代码操作到了成员变量(共享数据)时,就可能会出现多线程安全问题(线程不同步问题)。
编程技巧
在方法中尽量少操作成员变量(在不需要共享资源时),多使用局部变量
2、线程的同步
在Java语言中,引入对象互斥锁的概念,保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记保证在任一时刻,只能有一个线程访问对象。
关键字synchronized用来与对象的互斥锁关联。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问(这个对象就变成了同步对象)。
一个方法使用关键字synchronized修饰后,当一个线程A使用这个方法时,其他线程想使用这个方法就必须等待,直到线程A使用完该方法(前提是这些线程使用的是同一个同步对象)
3、同步方法的同步对象
1.对于非静态方法来说,this当前对象充当了同步对象
2.对于静态方法来说,同步对象是“类对象”,"类对象"代表的是这个类本身,所有通过类实例化的普通对象共享这个"类对象"
4、使用synchronized关键字实现同步(卖票问题)
synchronized的使用方法:
同步代码块:synchronized放在对象前面限制一段代码的执行
synchronized(同步对象){
需要同步的代码;
}
同步方法:synchronized放在方法声明中,表示
public synchronized void method(){
…
}
实例:
package lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketLock implements Runnable{
private int ticket=5;
private Lock lock=new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
lock.lock(); // 获取锁(更常用的方法)
//lock.tryLock();
try{
if(ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--; System.out.println(Thread.currentThread().getName()+"卖了一张票,当前剩余票数:"+ticket);
}
}finally{
lock.unlock(); // 释放锁
}
}
}
}
package lock;
public class TestLock {
public static void main(String[] args) {
TicketLock tl=new TicketLock();
new Thread(tl,"线程A").start();
new Thread(tl,"线程B").start();
new Thread(tl,"线程C").start();
}
}
1、加上同步机制后,效率低的原因:
1.会丧失Java多线程的并发优势。在执行到同步代码块(或同步方法)时,只能有一个线程执行,其他线程必须等待执行同步代码块(同步方法)的线程释放同步对象的锁。
2.其他等待锁释放的线程会不断检查锁的状态。
注意:
同步函数的使用的锁是this;
同步函数和同步代码块的区别:
同步函数的锁是固定的this。
同步代码块的锁是任意的对象。
建议使用同步代码块。
9、多线程状态图
多线程状态下的单例设计模式:
/*
多线程下的单例
*/
//饿汉式
class Single
{
private static final Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s;
}
}
//懒汉式
加入同步为了解决多线程安全问题。
加入双重判断是为了解决效率问题。
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
// -->0 -->1
s = new Single();
}
}
return s;
}
}
class SingleDemo
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}
一、死锁问题
1、死锁原因:
线程1锁住资源A等待资源B,线程2锁住资源B等待资源A,两个线程都在等待自己需要的资源,而这些资源被另外的线程锁住,这些线程你等我,我等你,谁也不愿意让出资源,这样死锁就产生了。
2、哲学家进餐问题
解决哲学家进餐问题代码示例:
package deadlock;
// 叉子类
public class Fork {
public void forkSay(){
System.out.println("我拿到叉子了,请给我刀子...");
}
}
package deadlock;
// 刀子类
public class Knife {
public void knifeSay(){
System.out.println("我拿到刀子了,请给我叉子...");
}
}
package deadlock;
// "哲学家进餐"
public class PhilosopherRunnable implements Runnable{
private boolean flag=false;
private static Knife knife=new Knife(); // 刀子资源
private static Fork fork=new Fork(); // 叉子资源
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized(knife){
knife.knifeSay();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(fork){
fork.forkSay();
}
}
System.out.println(Thread.currentThread().getName()+"吃完饭了......");
}else{
synchronized(fork){
fork.forkSay();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(knife){
knife.knifeSay();
}
}
System.out.println(Thread.currentThread().getName()+"吃完饭了......");
}
}
}
package deadlock;
public class Test {
public static void main(String[] args) {
PhilosopherRunnable p1=new PhilosopherRunnable();
p1.setFlag(true);
PhilosopherRunnable p2=new PhilosopherRunnable();
new Thread(p1,"尼采").start();
new Thread(p2,"苏格拉底").start();
}
}
一、Lock实现同步
java.util.concurrent.locks包下的相关接口与类
1. Lock接口
通常使用Lock接口中的方法用来获取锁,其中lock()方法是使用得最多的一个方法。
2. ReentrantLock类(“可重入锁”)
ReentrantLock是实现了Lock接口的类
3. ReadWriteLock接口
包括了两个方法:
Lock readLock(); 用来获取“读锁”
Lock writeLock(); 用来获取“写锁”
4. ReentrantReadWriteLock类
是ReadWriteLock接口的实现类
注意:
关于线程的读锁和写锁:
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁;但其他线程申请读锁是可以的。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
5.Synchronized与Lock
(1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
(2) synchronized在发生异常时,会自动释放线程占有的锁;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
(3) Lock可以提高多个线程进行读操作的效率
6.代码示例(利用多线程进行操作读写锁):
package rwlock;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class InfoRunnable implements Runnable {
private static int data;
private boolean flag=false;
private static ReadWriteLock rwl=new ReentrantReadWriteLock(); // 该属性对象可以获取"读锁"或"写锁"
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag){
for(int i=0;i<10;i++){
writeData();
}
}else{
for(int i=0;i<10;i++){
readData();
}
}
}
// 写数据方法
public static void writeData(){
try{
rwl.writeLock().lock(); // 获取"写锁"
System.out.println(Thread.currentThread().getName()+"准备写数据...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data=new Random().nextInt(10); // 模拟"写数据"
System.out.println(Thread.currentThread().getName()+"写数据完毕。");
}finally{
rwl.writeLock().unlock(); // 释放"写锁"
}
}
// 读数据方法
public static void readData(){
try{
rwl.readLock().lock(); // 获取"读锁"
System.out.println(Thread.currentThread().getName()+"准备读取数据...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"读取到的数据是:"+data);
}finally{
rwl.readLock().unlock(); // 释放"读锁"
}
}
}
package rwlock;
public class TestInfo {
public static void main(String[] args) {
InfoRunnable info1=new InfoRunnable();
info1.setFlag(true);
new Thread(info1,"写入线程1").start();
new Thread(info1,"写入线程2").start();
InfoRunnable info2=new InfoRunnable();
new Thread(info2,"读取线程1").start();
InfoRunnable info3=new InfoRunnable();
new Thread(info3,"读取线程2").start();
}
}
一、通过 Callable实现多线程
1. Callable接口介绍
java.util.concurrent.Callable是一个泛型接口,只有一个call()方法
call()方法抛出Exception异常,且返回一个指定的泛型类的对象
2. Callable接口实现线程的应用场景
当父线程想要获取子线程的运行结果时
3. 使用Callable接口实现多线程的步骤
第一步:创建Callable子类的实例化对象。
第二步:创建FutureTask 对象,并将Callable对象传入FutureTask的 构造方法中(注意:FutureTask实现了Runnable接口和Future接口)
第三步:实例化Thread对象,并在构造方法中传入FutureTask对象
第四步:启动线程
4. 生产者-消费者问题
生产者线程不断生产,消费者线程不断取走生产者生产的产品。
Object中的几个方法支持:
wait() 线程等待,当前线程进入调用对象的线程 等待池
notify() 唤醒1个等待线程
notifyAll() 唤醒全部等待的线程
注意:以上三个方法都必须在同步机制中调用
代码示例(一对一):
package one2one.producer;
// 早餐基础类
public class Breakfast {
private String food; // 吃的
private String drink; // 喝的
private boolean flag=false;
public synchronized void makeBreakfast(String food,String drink){
if(flag){
try {
wait(); // 生产者线程进入同步对象维护的“线程等待池”
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.food=food;
try {
Thread.sleep(1000); // 休眠,但不释放“锁”
} catch (InterruptedException e) {
e.printStackTrace();
}
this.drink=drink;
flag=true;
notify();
}
public synchronized void eatBreakfast(){
if(!flag){
try {
wait(); // 消费者线程进入同步对象维护的“线程等待池”,而且当前线程释放"锁"
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.food+"=============>"+this.drink);
flag=false;
notify();
}
}
package one2one.producer;
// 消费者线程
public class Consumer implements Runnable{
private Breakfast bf;
public Consumer(Breakfast bf){
this.bf=bf;
}
@Override
public void run() {
for (int i = 1; i <=7; i++) {
this.bf.eatBreakfast();
}
}
}
package one2one.producer;
// 生产者线程
public class Producer implements Runnable{
private Breakfast bf;
public Producer(Breakfast bf){
this.bf=bf;
}
@Override
public void run() {
for (int i = 1; i <=7; i++) {
if(i%2==0){
this.bf.makeBreakfast("bread","milk");
}else{
this.bf.makeBreakfast("馒头","稀饭");
}
}
}
}
package one2one.producer;
public class Test {
public static void main(String[] args) {
Breakfast bf=new Breakfast();
new Thread(new Producer(bf)).start(); // 启动生产者线程
new Thread(new Consumer(bf)).start(); // 启动消费者线程
}
}
5.多线程下载(复制)文件(代码示例)
package download;
import java.io.*;
public class DownloadRunnable implements Runnable{
private File srcFile; // 源文件路径
private long startPos; // 每个线程的开始下载位置
private long partTask; // 每个线程的下载任务
private RandomAccessFile raf; // 用来写入
public DownloadRunnable(File srcFile,long startPos,long partTask,RandomAccessFile raf){
this.srcFile=srcFile;
this.startPos=startPos;
this.partTask=partTask;
this.raf=raf;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"准备从第"+startPos+"个字节开始读...");
InputStream input=null;
try {
input=new FileInputStream(srcFile);
input.skip(startPos); // 跳过输入流的startPos个字节
byte[] b=new byte[1024*1024*10];
int len=0;
int count=0; // 用来记录已经读写的字节数
while((len=input.read(b))!=-1 && count<partTask){
raf.write(b, 0, len);
count+=len;
}
System.out.println(Thread.currentThread().getName()+"已经写入了"+count+"个字节");
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
input.close();
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package download;
import java.io.File;
import java.io.RandomAccessFile;
public class Client {
public static void main(String[] args)throws Exception {
File srcFile=new File("f:"+File.separator+"qmshy.mpg");
long partTask=srcFile.length()/6; // 准备使用6个线程,计算每个线程下载的任务
for(int i=0;i<6;i++){
RandomAccessFile raf=new RandomAccessFile("c:"+File.separator+srcFile.getName(),"rw");
long startPos=i*partTask; // 计算每条线程的读写起始位置
raf.seek(startPos); // 设置每条线程的文件指针偏移量
new Thread(new DownloadRunnable(srcFile,startPos,partTask,raf),i+"线程").start();
}
}
}