文档章节

[高并发Java 九] 锁的优化和注意事项

Hosee
 Hosee
发布于 2016/02/16 18:05
字数 4216
阅读 6415
收藏 34
点赞 3
评论 0

1. 锁优化的思路和方法

[高并发Java 一] 前言中有提到并发的级别。

一旦用到锁,就说明这是阻塞式的,所以在并发度上一般来说都会比无锁的情况低一点。

这里提到的锁优化,是指在阻塞式的情况下,如何让性能不要变得太差。但是再怎么优化,一般来说性能都会比无锁的情况差一点。

这里要注意的是,在[高并发Java 五] JDK并发包1中提到的ReentrantLock中的tryLock,偏向于一种无锁的方式,因为在tryLock判断时,并不会把自己挂起。

锁优化的思路和方法总结一下,有以下几种。

  • 减少锁持有时间 
  • 减小锁粒度
  • 锁分离 
  • 锁粗化 
  • 锁消除

1.1 减少锁持有时间 

public synchronized void syncMethod(){  
		othercode1();  
		mutextMethod();  
		othercode2(); 
	}
像上述代码这样,在进入方法前就要得到锁,其他线程就要在外面等待。

这里优化的一点在于,要减少其他线程等待的时间,所以,只用在有线程安全要求的程序上加锁

public void syncMethod(){  
		othercode1();  
		synchronized(this)
		{
			mutextMethod();  
		}
		othercode2(); 
	}

1.2 减小锁粒度

将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。

最最典型的减小锁粒度的案例就是ConcurrentHashMap。这个在[高并发Java 五] JDK并发包1有提到。

1.3 锁分离

最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发Java 五] JDK并发包1

读写分离思想可以延伸,只要操作互不影响,锁就可以分离。

比如LinkedBlockingQueue  

从头部取出,从尾部放数据。当然也类似于[高并发Java 六] JDK并发包2中提到的ForkJoinPool中的工作窃取。

1.4 锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

举个例子:

public void demoMethod(){  
		synchronized(lock){   
			//do sth.  
		}  
		//做其他不需要的同步的工作,但能很快执行完毕  
		synchronized(lock){   
			//do sth.  
		} 
	}
这种情况,根据锁粗化的思想,应该合并
public void demoMethod(){  
		//整合成一次锁请求 
		synchronized(lock){   
			//do sth.   
			//做其他不需要的同步的工作,但能很快执行完毕  
		}
	}
当然这是有前提的,前提就是中间的那些不需要同步的工作是很快执行完成的。

再举一个极端的例子:

for(int i=0;i<CIRCLE;i++){  
			synchronized(lock){  
				
			} 
		}
在一个循环内不同得获得锁。虽然JDK内部会对这个代码做些优化,但是还不如直接写成
synchronized(lock){ 
			for(int i=0;i<CIRCLE;i++){ 
				
			} 
		}
当然如果有需求说,这样的循环太久,需要给其他线程不要等待太久,那只能写成上面那种。如果没有这样类似的需求,还是直接写成下面那种比较好。

1.5 锁消除

锁消除是在编译器级别的事情。

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

也许你会觉得奇怪,既然有些对象不可能被多线程访问,那为什么要加锁呢?写代码时直接不加锁不就好了。

但是有时,这些锁并不是程序员所写的,有的是JDK实现中就有锁的,比如Vector和StringBuffer这样的类,它们中的很多方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提高性能。

比如:

public static void main(String args[]) throws InterruptedException {
		long start = System.currentTimeMillis();
		for (int i = 0; i < 2000000; i++) {
			createStringBuffer("JVM", "Diagnosis");
		}
		long bufferCost = System.currentTimeMillis() - start;
		System.out.println("craeteStringBuffer: " + bufferCost + " ms");
	}

	public static String createStringBuffer(String s1, String s2) {
		StringBuffer sb = new StringBuffer();
		sb.append(s1);
		sb.append(s2);
		return sb.toString();
	}
上述代码中的StringBuffer.append是一个同步操作,但是StringBuffer却是一个局部变量,并且方法也并没有把StringBuffer返回,所以不可能会有多线程去访问它。

那么此时StringBuffer中的同步操作就是没有意义的。

开启锁消除是在JVM参数上设置的,当然需要在server模式下:

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
并且要开启逃逸分析。 逃逸分析的作用呢,就是看看变量是否有可能逃出作用域的范围。

比如上述的StringBuffer,上述代码中craeteStringBuffer的返回是一个String,所以这个局部变量StringBuffer在其他地方都不会被使用。如果craeteStringBuffer改成

public static StringBuffer craeteStringBuffer(String s1, String s2) {
		StringBuffer sb = new StringBuffer();
		sb.append(s1);
		sb.append(s2);
		return sb;
	}
那么这个 StringBuffer被返回后,是有可能被任何其他地方所使用的(譬如被主函数将返回结果put进map啊等等)。那么JVM的逃逸分析可以分析出,这个局部变量 StringBuffer逃出了它的作用域。

所以基于逃逸分析,JVM可以判断,如果这个局部变量StringBuffer并没有逃出它的作用域,那么可以确定这个StringBuffer并不会被多线程所访问,那么就可以把这些多余的锁给去掉来提高性能。

当JVM参数为:

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
输出:

craeteStringBuffer: 302 ms
JVM参数为:

-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks
输出:

craeteStringBuffer: 660 ms
显然,锁消除的效果还是很明显的。

2. 虚拟机内的锁优化 

首先要介绍下对象头,在JVM中,每个对象都有一个对象头。

Mark Word,对象头的标记,32位(32位系统)。

描述对象的hash、锁信息,垃圾回收标记,年龄 

还会保存指向锁记录的指针,指向monitor的指针,偏向锁线程ID等。

简单来说,对象头就是要保存一些系统性的信息。

2.1 偏向锁

所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程 。

大部分情况是没有竞争的(某个同步块大多数情况都不会出现多线程同时竞争锁),所以可以通过偏向来提高性能。即在无竞争时,之前获得锁的线程再次获得锁时,会判断是否偏向锁指向我,那么该线程将不用再次获得锁,直接就可以进入同步块。

偏向锁的实施就是将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark 

当其他线程请求相同的锁时,偏向模式结束

JVM默认启用偏向锁 -XX:+UseBiasedLocking 

在竞争激烈的场合,偏向锁会增加系统负担(每次都要加一次是否偏向的判断) 

偏向锁的例子:

package test;

import java.util.List;
import java.util.Vector;

public class Test {
	public static List<Integer> numberList = new Vector<Integer>();

	public static void main(String[] args) throws InterruptedException {
		long begin = System.currentTimeMillis();
		int count = 0;
		int startnum = 0;
		while (count < 10000000) {
			numberList.add(startnum);
			startnum += 2;
			count++;
		}
		long end = System.currentTimeMillis();
		System.out.println(end - begin);
	}

}
Vector是一个线程安全的类,内部使用了锁机制。每次add都会进行锁请求。上述代码只有main一个线程再反复add请求锁。

使用如下的JVM参数来设置偏向锁:

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁。默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能。

由于这里为了测试偏向锁的性能,所以把延迟偏向锁的时间设置为0。

此时输出为9209

下面关闭偏向锁:

-XX:-UseBiasedLocking
输出为9627

一般在无竞争时,启用偏向锁性能会提高5%左右。

2.2 轻量级锁

Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意。

原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操作系统互斥(mutex)来实现的。

互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作。

为了优化Java的Lock机制,从Java6开始引入了轻量级锁的概念。

轻量级锁(Lightweight Locking)本意是为了减少多线程进入互斥的几率,并不是要替代互斥。

它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。

如果偏向锁失败,那么系统会进行轻量级锁的操作。它存在的目的是尽可能不用动用操作系统层面的互斥,因为那个性能会比较差。因为JVM本身就是一个应用,所以希望在应用层面上就解决线程同步问题。

总结一下就是轻量级锁是一种快速的锁定方法,在进入互斥之前,使用CAS操作来尝试加锁,尽量不要用操作系统层面的互斥,提高了性能。

那么当偏向锁失败时,轻量级锁的步骤:

1.将对象头的Mark指针保存到锁对象中(这里的对象指的就是锁住的对象,比如synchronized (this){},this就是这里的对象)。

lock->set_displaced_header(mark);

2.将对象头设置为指向锁的指针(在线程栈空间中)。

if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(),mark)) 
		 {       
			 TEVENT (slow_enter: release stacklock) ;       
			 return ; 
		 }
lock位于线程栈中。所以判断一个线程是否持有这把锁,只要判断这个对象头指向的空间是否在这个线程栈的地址空间当中。

如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁),就是操作系统层面的同步方法。在没有锁竞争的情况,轻量级锁减少传统锁使用OS互斥量产生的性能损耗。在竞争非常激烈时(轻量级锁总是失败),轻量级锁会多做很多额外操作,导致性能下降。

2.3 自旋锁

当竞争存在时,因为轻量级锁尝试失败,之后有可能会直接升级成重量级锁动用操作系统层面的互斥。也有可能再尝试一下自旋锁。

如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋),并且不停地尝试拿到这个锁(类似tryLock),当然循环的次数是有限制的,当循环次数达到以后,仍然升级成重量级锁。所以在每个线程对于锁的持有时间很少时,自旋锁能够尽量避免线程在OS层被挂起。

JDK1.6中-XX:+UseSpinning开启

JDK1.7中,去掉此参数,改为内置实现 

如果同步块很长,自旋失败,会降低系统性能。如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能。

2.4 偏向锁,轻量级锁,自旋锁总结 

上述的锁不是Java语言层面的锁优化方法,是内置在JVM当中的。

首先偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。

而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。

为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。

可见偏向锁,轻量级锁,自旋锁都是乐观锁。

3. 一个错误使用锁的案例 

public class IntegerLock {
	static Integer i = 0;

	public static class AddThread extends Thread {
		public void run() {
			for (int k = 0; k < 100000; k++) {
				synchronized (i) {
					i++;
				}
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		AddThread t1 = new AddThread();
		AddThread t2 = new AddThread();
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(i);
	}
}
一个很初级的错误在于,在 [高并发Java 七] 并发设计模式提到,Interger是final不变的,每次++后,会产生一个新的 Interger再赋给i,所以两个线程争夺的锁是不同的。所以并不是线程安全的。

4. ThreadLocal及其源码分析 

这里来提ThreadLocal可能有点不合适,但是ThreadLocal是可以把锁代替的方式。所以还是有必要提一下。

基本的思想就是,在一个多线程当中需要把有数据冲突的数据加锁,使用ThreadLocal的话,为每一个线程都提供一个对象实例。不同的线程只访问自己的对象,而不访问其他的对象。这样锁就没有必要存在了。

package test;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
	private static final SimpleDateFormat sdf = new SimpleDateFormat(
			"yyyy-MM-dd HH:mm:ss");

	public static class ParseDate implements Runnable {
		int i = 0;

		public ParseDate(int i) {
			this.i = i;
		}

		public void run() {
			try {
				Date t = sdf.parse("2016-02-16 17:00:" + i % 60);
				System.out.println(i + ":" + t);
			} catch (ParseException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		ExecutorService es = Executors.newFixedThreadPool(10);
		for (int i = 0; i < 1000; i++) {
			es.execute(new ParseDate(i));
		}
	}

}
由于SimpleDateFormat并不线程安全的,所以上述代码是错误的使用。最简单的方式就是,自己定义一个类去用synchronized包装(类似于Collections.synchronizedMap)。这样做在高并发时会有问题,对 synchronized的争用导致每一次只能进去一个线程,并发量很低。

这里使用ThreadLocal去封装SimpleDateFormat就解决了这个问题

package test;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
	static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();

	public static class ParseDate implements Runnable {
		int i = 0;

		public ParseDate(int i) {
			this.i = i;
		}

		public void run() {
			try {
				if (tl.get() == null) {
					tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
				}
				Date t = tl.get().parse("2016-02-16 17:00:" + i % 60);
				System.out.println(i + ":" + t);
			} catch (ParseException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		ExecutorService es = Executors.newFixedThreadPool(10);
		for (int i = 0; i < 1000; i++) {
			es.execute(new ParseDate(i));
		}
	}

}

每个线程在运行时,会判断是否当前线程有SimpleDateFormat对象

if (tl.get() == null)
如果没有的话,就new个 SimpleDateFormat与当前线程绑定

tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
然后用当前线程的 SimpleDateFormat去解析

tl.get().parse("2016-02-16 17:00:" + i % 60);
一开始的代码中,只有一个 SimpleDateFormat,使用了 ThreadLocal,为每一个线程都new了一个 SimpleDateFormat。

需要注意的是,这里不要把公共的一个SimpleDateFormat设置给每一个ThreadLocal,这样是没用的。需要给每一个都new一个SimpleDateFormat。

在hibernate中,对ThreadLocal有典型的应用。

下面来看一下ThreadLocal的源码实现

首先Thread类中有一个成员变量:

ThreadLocal.ThreadLocalMap threadLocals = null;

而这个Map就是ThreadLocal的实现关键

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
根据 ThreadLocal可以set和get相对应的value。

这里的ThreadLocalMap实现和HashMap差不多,但是在hash冲突的处理上有区别。

ThreadLocalMap中发生hash冲突时,不是像HashMap这样用链表来解决冲突,而是是将索引++,放到下一个索引处来解决冲突。



系列:

[高并发Java 一] 前言

[高并发Java 二] 多线程基础

[高并发Java 三] Java内存模型和线程安全

[高并发Java 四] 无锁

[高并发Java 五] JDK并发包1

[高并发Java 六] JDK并发包2

[高并发Java 七] 并发设计模式

[高并发Java 八] NIO和AIO

[高并发Java 九] 锁的优化和注意事项

[高并发Java 十] JDK8对并发的新支持



Reference:

1. http://blog.csdn.net/songylwq/article/details/5585734

© 著作权归作者所有

共有 人打赏支持
Hosee
粉丝 509
博文 132
码字总数 207228
作品 0
杭州
程序员
Java 多线程编程 — 锁优化2

Java多线程编程-(13)- 关于锁优化的几点建议 一、背景 在《 Java多线程编程-(11)-从volatile和synchronized的底层实现原理看Java虚拟机对锁优化所做的努力》 这一篇文章中,我们大致介绍...

晨猫 ⋅ 04/26 ⋅ 0

面试必看!2018年4月份阿里最新的java程序员面试题目

目录 技术一面(23问) 技术二面(3大块) 性能优化(21点) 项目实战(34块) JAVA方向技术考察点(15点) JAVA开发技术面试中可能问到的问题(17问) 阿里技术面试1 1.Java IO流的层次结构...

美的让人心动 ⋅ 04/16 ⋅ 0

Java程序员面试大纲—错过了金三银四,你还要错过2018吗?

跳槽时时刻刻都在发生,但是我建议大家跳槽之前,先想清楚为什么要跳槽。切不可跟风,看到同事一个个都走了,自己也盲目的开始面试起来(期间也没有准备充分),到底是因为技术原因(影响自己...

java高级架构牛人 ⋅ 04/27 ⋅ 0

【Java并发专题】27篇文章详细总结Java并发基础知识

努力的意义,就是,在以后的日子里,放眼望去全是自己喜欢的人和事! github:https://github.com/CL0610/Java-concurrency,欢迎题issue和Pull request。所有的文档都是自己亲自码的,如果觉...

你听___ ⋅ 05/06 ⋅ 0

悲观的并发策略——synchronized互斥锁

互斥锁是最常见的同步手段,在并发过程中,当多条线程对同一个共享数据竞争时,它保证共享数据同一时刻只能被一条线程使用,其他线程只有等到锁释放后才能重新进行竞争。 对于Java开发人员,...

wangyangzhizhou ⋅ 04/16 ⋅ 0

Java 编程之美:并发编程基础晋级篇

本文来自作者 加多 在 GitChat 上分享 「Java 并发编程之美:并发编程基础晋级篇」 编辑 | Mc Jin 借用 Java 并发编程实践中的话,编写正确的程序并不容易,而编写正常的并发程序就更难了! ...

gitchat ⋅ 04/18 ⋅ 0

并发编程之——读锁源码分析(解释关于锁降级的争议)

1. 前言 在前面的文章 并发编程之——写锁源码分析中,我们分析了 1.8 JUC 中读写锁中的写锁的获取和释放过程,今天来分析一下读锁的获取和释放过程,读锁相比较写锁要稍微复杂一点,其中还有...

stateIs0 ⋅ 01/12 ⋅ 0

Java并发机制及锁的实现原理

Java并发编程概述 并发编程的目的是为了让程序运行得更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面...

烂猪皮 ⋅ 05/11 ⋅ 0

书单丨5本Java后端技术书指引你快速进阶

一名Java开发工程师 不仅要对Java语言及特性有深层次的理解 而且需要掌握与Java相关的 框架、生态及后端开发知识 本文涉及多种后端开发需要掌握的技能 对于帮助提高开发能力非常有帮助 NO.1...

Java高级架构 ⋅ 05/30 ⋅ 0

java基础thread——java5之后的多线程(浅尝辄止)

承上启下 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象L...

潇潇漓燃 ⋅ 06/03 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

645. Set Mismatch - LeetCode

Question 645. Set Mismatch Solution 思路: 遍历每个数字,然后将其应该出现的位置上的数字变为其相反数,这样如果我们再变为其相反数之前已经成负数了,说明该数字是重复数,将其将入结果r...

yysue ⋅ 24分钟前 ⋅ 0

Confluence 6 从生产环境中恢复一个测试实例

请参考 Restoring a Test Instance from Production 页面中的内容获得更多完整的说明。 很多 Confluence 的管理员将会使用生产实例运行完整数据和服务的 Confluence 服务器,同时还会设置一个...

honeymose ⋅ 28分钟前 ⋅ 0

Python这么强?红包杀手、消息撤回也可以无视,手机App辅助!

论述 标题也许有点不好理解,其实就是一款利用Python实现的可以监控微信APP内的红包与消息撤回的助手。不得不说,这确实是一款大家钟意的神器。 消息撤回是一件很让人恶心的事,毕竟人都是有...

Python燕大侠 ⋅ 40分钟前 ⋅ 0

压缩打包介绍、gzip压缩工具、bzip2压缩工具、xz压缩工具

压缩打包介绍 压缩的好处不仅能节省磁盘空间而且在传输的时候节省传输时间和网络带宽 windows系统下文件带有 .rar .zip .7z 后缀的就是压缩文件 linux系统下则是 .zip, .gz, .bz2, .xz, ...

黄昏残影 ⋅ 44分钟前 ⋅ 0

观察者模式

1.利用java原生类进行操作 package observer;import java.util.Observable;import java.util.Observer;/** * @author shadow * @Date 2016年8月12日下午7:29:31 * @Fun 观察目标 **/......

Cobbage ⋅ 47分钟前 ⋅ 0

Ubuntu打印服务器配置

参考:https://blog.csdn.net/gsls200808/article/details/50950586 https://blog.csdn.net/jiay2/article/details/80252369 https://wiki.gentoo.org/wiki/HPLIP 由于媳妇儿要大量打印资料,......

大熊猫 ⋅ 53分钟前 ⋅ 0

面试的角度诠释Java工程师(二)

原文出处: locality 续言: 相信每一位简书的作者,都会有我这样的思考:怎么写好一篇文章?或者怎么写好一篇技术类的文章?我就先说说我的感悟吧,写文章其实和写程序是一样的。为什么我会...

颖伙虫 ⋅ 55分钟前 ⋅ 0

github中SSH的Key

https://help.github.com/articles/connecting-to-github-with-ssh/ https://help.github.com/articles/testing-your-ssh-connection/ https://help.github.com/articles/adding-a-new-ssh-k......

whoisliang ⋅ 56分钟前 ⋅ 0

only_full_group_by

我的mysql是在CentOS7.1下面的5.7.17 在 /etc/my.cnf 文件里加上如下: sql_mode='NO_ENGINE_SUBSTITUTION' 然后,重启Mysql服务 systemctl restart mysqld...

SunHacker ⋅ 今天 ⋅ 0

实际项目(SpringBoot项目)中集成Druid

参考网页 https://blog.csdn.net/liuchuanhong1/article/details/55050131 https://blog.csdn.net/CoffeeAndIce/article/details/78707819 https://www.pocketdigi.com/20170530/1577.html 为......

karma123 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部