程序、进程、线程
- 程序:一段静态代码
- 进程:程序的一次执行过程,正在运行的一个程序。是资源分配的基本单位
- 线程:一个进程可分为多个线程。线程是调度和执行的基本单位。每个线程拥有独立的运行栈和程序计数器。线程之间切换开销小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间。他们从堆中共享同一对象,可以访问相同的变量和对象。这使得线程间通信更简便高效。但是也会带来进程同步的安全隐患
- 内存解析:
- 虚拟机栈、程序计数器是每个线程一份
- 方法区、堆是每个进程一份
多线程的创建
方式一:继承Thread类
/*创建步骤:
1.创建一个继承于Thread类的子类
2.重写Thread类的run(),将线程执行的操作声明在run()中
3.创建Thread类的子类对象
通过此对象调用start()
*/
//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
//2.重写Thread类的run()
@Override
public void run(){
//方法体
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread t1 = new MyThread();
//4.通过此对象调用start():(1)启动当前线程(2)调用当前线程的run()
t1.start();
//继续创建新线程
MyThread t1 = new MyThread();
t2.start();
//一下操作依然是在主线程中执行
System.out.println(Thread.currentThread().getName());
}
}
附:下面程序展示了创建Thread类的匿名子类的方式
new Thread(){
@Override
public void run() {
//方法体
}
}
}
}.start();
方式二:实现Runnable接口
/*创建步骤:
1.创建一个实现了Runnable接口的类
2.实现类去实现Runnable中的抽象方法run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
*/
//1.创建一个实现了Runnable接口的类
class MThread implements Runnable{
//2.实现类去实现Runnable中的抽象方法run()
@Override
public void run(){
//方法体
}
}
public class ThreadTest{
public static void main(String[] args){
//3.创建实现类的对象
MThread mThread = new MThread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mThread);
//5.通过此对象调用start()
t1.start();
//再启动一个线程
Thread t1 = new Thread(mThread);
t2.start();
比较上述两种方式:
一般优先选择实现Runnable接口的方式
原因:1.实现的方式没有类的单继承局限性
2.实现的方式更适合来处理多个线程有共享数据的情况
联系:public class Thread implements Runnable
相同点:两者都需要重写run(),将线程要执行的逻辑声明在run()中
方式三:实现Callable接口(JDK 5.0新增)
/*创建步骤:
1.创建一个实现Callable接口的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建Callable接口实现类的对象
4.将创建的实现类对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6.可以通过get()获取Callable中call()方法的返回值
*/
//1.创建一个实现Callable接口的实现类
class MyThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception{
//方法体
return object;//可以根据需要选择是否返回、返回什么
}
}
public class ThreadTest{
public static void main(String[] args){
//3.创建Callable接口实现类的对象
MyThread myThread = new MyThread();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象
FutureTask futureTask = new FutureTask(myThread);
//5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
//6.可以通过get()获取Callable中call方法的返回值
try{
Object object = futureTask.get();
//方法体
} catch(InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如何理解实现Callable接口的方式比实现Runnable接口的方式更强大?
1.call()可以有返回值
2.call()可以抛出异常
3.Callable是支持泛型的
方式四:使用线程池(JDK 5.0新增)
经常创建和销毁使用量特别大的线程,对性能影响很大。可以提前创建好多个线程放入线程池中,使用时直接获取,用完放回池中,可以避免频繁销毁,实现重复利用
/*创建步骤:
1.提供指定线程数量的线程池
2.执行指定的线程的操作。需要提供显示Runnable接口或Callable接口实现类的对象
3.关闭连接池
*/
class MyThread implements Runnable{
@Override
public void run(){
//方法体
}
}
class MyThread1 implements Runnable{
@Override
public void run(){
//方法体
}
}
public class ThreadPool{
public static void main(String[] args){
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
//设置线程池的属性
service1.setCorePoolSize(15);//设置核心池大小
service1.setKeepAliveTime();//设置无任务时线程最多存活多长时间
//2.执行指定的线程操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new MyThread());
service.execute(new MyThread1());
//service.submit(Callable callable);这是Callable的写法
//3.关闭连接池
service.shutdown();
}
}
使用线程池的好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
3.便于线程管理
Thread中的常用方法
start()
:启动当前线程,调用当前线程的run()
currentThread()
:静态方法,返回执行当前代码的线程getName()
:获取当前线程的名字setName()
:设置当前线程的名字yield()
:释放当前CPU的执行权,把执行权让给优先级相同或更高的线程。static
方法,直接调用join()
:在线程a中调用线程b的join()
,此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态sleep(long millitime)
:让当前线程阻塞指定的millitime
毫秒,static
方法,直接调用isAlive()
:判断当前线程是否存活
线程的优先级
MAX_PRIORITY
:10;
MIN_PRIORITY
:1;
NORM_PRIORITY
:5(默认优先级)- getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级 - 高优先级抢占低优先级线程的CPU执行权,只是从概率上来说,高优先级的线程高概率被调度执行,并不意味着只有高优先级执行完后低优先级才能执行
线程的分类
- 分为守护线程和用户线程
- 它们的区别是判断JVM何时离开(不懂,先挖个坑在这吧)
线程同步机制
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
- 操作共享数据的代码,即为需要被同步的代码
- 共享数据:多个线程共同操作的变量
- 同步监视器,俗称“锁”。锁必须是唯一的,才能起到同步作用
- 关于锁的选择:
- 任何对象都可以作为同步锁,比如自己随便new一个对象当锁。因为所有对象都自动含有单一的锁
- 一个线程类中,所有静态方法共用同一把锁(
类名.class
),所有非静态方法共用同一把锁(this
) - 一定要确保同一共享数据的多个线程共用一把锁
方式二:同步方法
//把synchronized放在方法生命中,表示整个方法为同步方法
public synchronized void method(){
//需要被同步的方法体
}
个人觉得,其实同步方法可以理解为,把同步代码块中的需要被同步的代码抽取出来,写成一个方法,加上一个
synchronized
声明,做成同步方法,扔给run()
方法调用。本质上两种方法都一样的,都是“synchronized
+锁”来保证同步。
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明,自动选择了this
或当前类.class
当同步监视器
方式三:Lock(JDK 5.0开始)
//以实现Runnable接口方式为例
//实例化ReentrantLock
private ReentrantLock lock = new ReenTrantLock();//这里的ReentrantLock()可以有参数,有的话就是boolean fair参数,表示是否遵守“先来先服务”
//如果是继承Thread类的创建方法,此处ReentrantLock必须是static的,否则每个对象都会有一个lock
public void method(){
lock.lock();//上锁
try{//lock一般都要放在try结构中,即使没有异常也要使用try-finally结构,因为要保证unlock一定会被执行
//保证线程安全的代码;
}
finally{//需要把unlock写入finally中以保证锁一定会被释放
lock.unlock();//解锁
}
}
synchronized与Lock的对比
Lock
是显式锁,手动开启/关闭;synchronized
是隐式锁,出了作用域自动释放Lock
只有代码块锁,synchronized
有代码块锁和方法锁Lock
没有同步监视器,synchronized
需要唯一的同步监视器- 使用
Lock
锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类) - 优先使用顺序:
Lock
—>同步代码块—>同步方法
改写懒汉式单例模式
class SingleInstance{
private SingleInstance(){}
private static SingelInstance singelInstace = null;
public static SingleInstance getInstance(){
if(singelInstance == null){
synchronized(SingleInstance.class){
if(singelInstance == null){
singleInstance = new SingleInstance();
}
}
}
return singleInstance;
}
}
死锁
不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
同步机制小结
- 使用继承Thread类方法创建多线程时,由于会创建多个当前类对象,每一个线程都会有一个
this
,所以最好使用类.class
当同步监视器 - 使用实现
Runnable
接口方法创建多线程时,只创建了一个当前类对象,所以可以使用this
充当同步监视器 - 在选择需要同步的代码范围时,要合理进行选择。范围太小,不能起到同步作用;范围太大,限制了多线程的性能
- 当前线程在同步时遇到
break
、return
、各种异常、wait()
等,都会暂停线程并释放锁。但是遇到sleep()
、yield()
、suspend()
(挂起)时,不会释放锁 - 同步的优缺点:
- 优点:解决了线程的安全问题
- 缺点:操作同步代码时,只能有一个线程参与,其它线程等待。相当于是一个单线程过程,效率较低
同步机制练习
程序实现:银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱
public synchronized void deposit(double amt){
if(amt > 0){
balance += amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
}
}
}
class Customer extends Thread{
private Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
线程通信
三个方法
wait()
:当前进程就进入阻塞状态,并释放同步监视器notify()
:唤醒被wait()
阻塞的一个进程。如果有多个线程被wait
,则唤醒优先级最高的那个notifyAll()
:唤醒所有被wait
阻塞的线程- 一般
wait()
要和notify()
或notifyAll()
搭配使用 - 一个线程被
wait
后再被notify
,会从上次wait
的地方继续执行 - 这三个方法必须使用在同步代码块或同步方法中,并且调用者必须是同步监视器。所以
lock
方法中无法使用 - 这三个方法是定义在
Object
类中的
sleep()和wait()的异同:
相同点:都会使当前线程进入阻塞状态
不同点:1.声明位置:sleep
声明在Thread
类中,wait
声明在Object
类中
2.调用场景不同:sleep
可以在任何需要的地方调用,wait
只能在同步方法、同步代码块中被调用
3.释放同步监视器:如果都在同步代码块或同步方法中,sleep
不会释放锁,wait
会释放锁
代码示例:
/**
* 线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
*/
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
obj.notify();
if(number <= 100){
try {
Thread.sleep(10);//加sleep目的是,如果有安全问题,增大问题暴露概率
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
生产者消费者问题
/**
* 线程通信的应用:经典例题:生产者/消费者问题
*
* 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
* 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员
* 会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品
* 了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
*
* 分析:
* 1. 是否是多线程问题?是,生产者线程,消费者线程
* 2. 是否有共享数据?是,店员(或产品)
* 3. 如何解决线程的安全问题?同步机制,有三种方法
* 4. 是否涉及线程的通信?是
*/
class Clerk{
private int productCount = 0;
//生产产品
public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{//生产者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品.....");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消费者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品.....");
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
Consumer c2 = new Consumer(clerk);
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}